diff --git a/js/i18n/locales/en-US/common.ftl b/js/i18n/locales/en-US/common.ftl index f5f01962b..5ab551af9 100644 --- a/js/i18n/locales/en-US/common.ftl +++ b/js/i18n/locales/en-US/common.ftl @@ -201,6 +201,92 @@ find-streamers = Find streamers by name or handle something-went-wrong = Something went wrong unexpected-error = An unexpected error occurred. +## Web App - Upload +upload-add-tag = Add tag, press Enter +upload-add-thumbnail = Add thumbnail image +upload-cancel = Cancel +upload-choose-file = Choose a video file +upload-choose-file-btn = Choose file +upload-content-warnings = Content Warnings +upload-delete = Delete +upload-deleting = Deleting... +upload-description = Description +upload-description-placeholder = Describe your video... +upload-details = Details +upload-file = File +upload-license = License +upload-optional-info = Optional · JPG, PNG up to 975KB +upload-preparing = Preparing upload... +upload-processing = Processing video +upload-publish = Publish +upload-published = Published +upload-publishing = Publishing... +upload-publishing-btn = Publishing... +upload-ready-publish = Ready to publish +upload-status = Status +upload-tags = Tags +upload-thumbnail = Thumbnail +upload-thumbnail-preview = Thumbnail preview +upload-title = Title +upload-title-placeholder = Give your video a title +upload-update-video = Update Video +upload-updating = Updating... +upload-view-video = View video → +upload-waiting-process = Waiting to process... + +## Web App - Stream Key +create-key-description-delete-if-exposed = If you think your stream key has been exposed, delete it immediately and create a new key. +create-key-description-staff = We will never, ever, ask for your key. +create-key-do-not-share = DO NOT SHARE THIS KEY, including showing it on stream. Anyone in possession of your key may be able to stream from your account. +create-key-title = Are you sure about creating a new stream key? +create-stream-key = Create Stream Key +stream-key = Stream Key +stream-key-help = Your stream key identifies you to Streamplace. You can have more than one stream key at a time, and it's a good idea to prune old keys that you aren't using via the key manager. +stream-key-info-description = For security reasons, you won't be able to view it again through your account. If you lose this stream key, you'll need to generate a new one. +stream-key-label = Stream Key +stream-key-save-now = Please save this stream key somewhere safe. + +## Web App - Metadata & Settings +allow-everyone-archive = Allow everyone to archive your content indefinitely +allow-everyone-distribute = Allow everyone to distribute your content +allowed-broadcasters = Allowed Broadcasters +allowed-broadcasters-help = Enter the DIDs of the broadcasters you want to allow, one per line. +content-rights = Content Rights +content-rights-help = Optional copyright and license information for your stream. +content-warnings = Content Warnings +content-warnings-help = You're required to flag your stream with themes that viewers may want a heads-up about. +copied-to-clipboard = Copied to clipboard +copy-link = Copy link +copyright-notice = Copyright Notice +copyright-year = Copyright Year +credit-line = Credit Line +custom-license = Custom… +custom-license-url = Custom License URL/Text +delete-after = Delete After +delete-after-help = Duration in seconds (e.g. 300 for 5 minutes). +distribution = Distribution +distribution-help = Control who can redistribute your stream and for how long archives are kept. +editing-video = Editing video +failed-to-create-key = Failed to create key +go-to-video = Go to video +key-manager = Key Manager +license = License +login-to-manage-videos = Please log in to manage your videos. +login-to-upload = Please log in to upload videos. +metadata = Metadata +metadata-learn-more = Learn more about content metadata +metadata-save-failed = Failed to save metadata +metadata-saved = Metadata saved +moderation = Moderation +moderation-coming-soon = Moderator management hooks aren't on the web yet — this section is a placeholder. +moderation-help = Add/remove stream moderators. Moderators can hide chat messages and time out users in your chat. +nav-dashboard = Dashboard +nav-settings = Settings +no-videos-yet-upload = No videos yet. +optional = Optional +required = Required +select-license = Select a license… + ## Login login-show-live-on-bluesky = Show when I'm live on Bluesky login-show-live-on-bluesky-description = Gives your Bluesky avatar the red LIVE ring while you stream and lets Streamplace post announcements for you. Uncheck to sign in without granting any access to your Bluesky account. diff --git a/js/i18n/locales/en-US/settings.ftl b/js/i18n/locales/en-US/settings.ftl index 34025df04..5658cc657 100644 --- a/js/i18n/locales/en-US/settings.ftl +++ b/js/i18n/locales/en-US/settings.ftl @@ -335,3 +335,23 @@ chat-profile-preview-message = Hello, world! chat-profile-self-labels = Self Labels chat-profile-label-bot = Bot chat-profile-label-bot-description = Mark your chat messages as coming from a bot. + +## Web App - Dashboard +control-panel = Control Panel +create-stream-key-in-stream-settings = Create a stream key in Stream Settings +creator-settings = Creator Settings +delete-all-keys = Delete All Keys +delete-all-keys-btn = Delete All +delete-all-keys-description = This will permanently remove all {$count} stream keys. Any streaming software configured with these keys will stop working. +delete-all-keys-title = Delete all stream keys? +failed-delete-keys = Failed to delete {$count} key(s) +failed-to-delete-key = Failed to delete key +my-videos = My Videos +stream-settings = Stream Settings +stream-settings-link = Stream Settings +time-ago-created = made {$timeAgo}{$by ? ` by ${createdBy}` : ``} +time-ago-days = {$days}d ago +time-ago-hours = {$hours}h ago +time-ago-minutes = {$minutes}m ago +time-ago-seconds = {$seconds}s ago +upload-videos = Upload Videos diff --git a/js/i18n/public/locales/en-US/common.json b/js/i18n/public/locales/en-US/common.json index 44cb58fc9..f3bf7c046 100644 --- a/js/i18n/public/locales/en-US/common.json +++ b/js/i18n/public/locales/en-US/common.json @@ -137,5 +137,85 @@ "no-results-for": "No results for \"{$query}\"", "find-streamers": "Find streamers by name or handle", "something-went-wrong": "Something went wrong", - "unexpected-error": "An unexpected error occurred." + "unexpected-error": "An unexpected error occurred.", + "upload-add-tag": "Add tag, press Enter", + "upload-add-thumbnail": "Add thumbnail image", + "upload-cancel": "Cancel", + "upload-choose-file": "Choose a video file", + "upload-choose-file-btn": "Choose file", + "upload-content-warnings": "Content Warnings", + "upload-delete": "Delete", + "upload-deleting": "Deleting...", + "upload-description": "Description", + "upload-description-placeholder": "Describe your video...", + "upload-details": "Details", + "upload-file": "File", + "upload-license": "License", + "upload-optional-info": "Optional · JPG, PNG up to 975KB", + "upload-preparing": "Preparing upload...", + "upload-processing": "Processing video", + "upload-publish": "Publish", + "upload-published": "Published", + "upload-publishing": "Publishing...", + "upload-publishing-btn": "Publishing...", + "upload-ready-publish": "Ready to publish", + "upload-status": "Status", + "upload-tags": "Tags", + "upload-thumbnail": "Thumbnail", + "upload-thumbnail-preview": "Thumbnail preview", + "upload-title": "Title", + "upload-title-placeholder": "Give your video a title", + "upload-update-video": "Update Video", + "upload-updating": "Updating...", + "upload-view-video": "View video →", + "upload-waiting-process": "Waiting to process...", + "create-key-description-delete-if-exposed": "If you think your stream key has been exposed, delete it immediately and create a new one!", + "create-key-description-staff": "Staff will never, ever ask for your stream key!", + "create-key-do-not-share": "DO NOT SHARE THIS KEY OR SHOW IT ON STREAM, as ANYONE with this key may be able to stream from your account!", + "create-key-title": "Are you sure about creating a new stream key?", + "create-stream-key": "Create Stream Key", + "stream-key": "Stream Key", + "stream-key-help": "Your stream key identifies you to Streamplace. You can have more than one stream key at a time, and it's a good idea to prune old keys that you aren't using via the key manager.", + "stream-key-info-description": "For security reasons, you won't be able to view it again through your account. If you lose this stream key, you'll need to generate a new one.", + "stream-key-label": "Stream Key", + "stream-key-save-now": "Please save this stream key somewhere safe.", + "allow-everyone-archive": "Allow everyone to archive your content indefinitely", + "allow-everyone-distribute": "Allow everyone to distribute your content", + "allowed-broadcasters": "Allowed Broadcasters", + "allowed-broadcasters-help": "Enter the DIDs of the broadcasters you want to allow, one per line.", + "content-rights": "Content Rights", + "content-rights-help": "Optional copyright and license information for your stream.", + "content-warnings": "Content Warnings", + "content-warnings-help": "You're required to flag your stream with themes that viewers may want a heads-up about.", + "copied-to-clipboard": "Copied to clipboard", + "copy-link": "Copy link", + "copyright-notice": "Copyright Notice", + "copyright-year": "Copyright Year", + "credit-line": "Credit Line", + "custom-license": "Custom…", + "custom-license-url": "Custom License URL/Text", + "delete-after": "Delete After", + "delete-after-help": "Duration in seconds (e.g. 300 for 5 minutes).", + "distribution": "Distribution", + "distribution-help": "Control who can redistribute your stream and for how long archives are kept.", + "editing-video": "Editing video", + "failed-to-create-key": "Failed to create key", + "go-to-video": "Go to video", + "key-manager": "Key Manager", + "license": "License", + "login-to-manage-videos": "Please log in to manage your videos.", + "login-to-upload": "Please log in to upload videos.", + "metadata": "Metadata", + "metadata-learn-more": "Learn more about content metadata", + "metadata-save-failed": "Failed to save metadata", + "metadata-saved": "Metadata saved", + "moderation": "Moderation", + "moderation-coming-soon": "Moderator management hooks aren't on the web yet — this section is a placeholder.", + "moderation-help": "Add/remove stream moderators. Moderators can hide chat messages and time out users in your chat.", + "nav-dashboard": "Dashboard", + "nav-settings": "Settings", + "no-videos-yet-upload": "No videos yet.", + "optional": "Optional", + "required": "Required", + "select-license": "Select a license…" } diff --git a/js/i18n/public/locales/en-US/settings.json b/js/i18n/public/locales/en-US/settings.json index 7a21952c0..457536511 100644 --- a/js/i18n/public/locales/en-US/settings.json +++ b/js/i18n/public/locales/en-US/settings.json @@ -251,5 +251,22 @@ "chat-profile-preview-message": "Hello, world!", "chat-profile-self-labels": "Self Labels", "chat-profile-label-bot": "Bot", - "chat-profile-label-bot-description": "Mark your chat messages as coming from a bot." + "chat-profile-label-bot-description": "Mark your chat messages as coming from a bot.", + "control-panel": "Control Panel", + "create-stream-key-in-stream-settings": "Create a stream key in Stream Settings", + "creator-settings": "Creator Settings", + "delete-all-keys": "Delete All Keys", + "delete-all-keys-btn": "Delete All", + "delete-all-keys-description": "This will permanently remove all {$count} stream keys. Any streaming software configured with these keys will stop working.", + "delete-all-keys-title": "Delete all stream keys?", + "failed-delete-keys": "Failed to delete {$count} key(s)", + "failed-to-delete-key": "Failed to delete key", + "my-videos": "My Videos", + "stream-settings": "Stream Settings", + "stream-settings-link": "Stream Settings", + "time-ago-days": "{$days}d ago", + "time-ago-hours": "{$hours}h ago", + "time-ago-minutes": "{$minutes}m ago", + "time-ago-seconds": "{$seconds}s ago", + "upload-videos": "Upload Videos" } diff --git a/js/web/package.json b/js/web/package.json index e093dad65..147293815 100644 --- a/js/web/package.json +++ b/js/web/package.json @@ -9,12 +9,13 @@ "predev": "pnpm gen:emoji", "build": "tsc --noEmit && vite build", "preview": "vite preview --port 5173", - "check": "tsc --noEmit", + "check": "bash -c 'set -euo pipefail;export OUT=$(mktemp -d); npx tsc -p . --outDir $OUT; rm -rf $OUT'", "gen:emoji": "node scripts/gen-emoji-data.mjs", "prebuild": "pnpm gen:emoji" }, "dependencies": { "@atproto/api": "^0.19.3", + "@atproto/crypto": "^0.4.5", "@atproto/oauth-client-browser": "^0.4.1", "@base-ui/react": "^1.5.0", "@dnd-kit/helpers": "^0.4.0", @@ -48,7 +49,9 @@ "sonner": "^2.0.7", "streamplace": "workspace:*", "tiptap": "^1.32.2", + "tus-js-client": "^4.3.1", "tw-animate-css": "^1.4.0", + "viem": "^2.47.2", "zod": "^3.23.0", "zustand": "^5.0.5" }, diff --git a/js/web/plan.md b/js/web/plan.md index 133fef621..8896acd3f 100644 --- a/js/web/plan.md +++ b/js/web/plan.md @@ -12,9 +12,10 @@ The existing React Native app (`js/app`) is the source of truth. The new web app - Single live stream viewer (HLS + WebRTC) - Single VOD viewer - VOD browse +- VOD upload (tus-js-client, metadata, thumbnails, progress) - OAuth login/registration - Chat (send/receive, mentions, emoji) -- Settings: account, streaming (read-only keys), recommendations, webhooks, multistream, privacy, backup, branding, badges, languages, advanced, about +- Settings: account, streaming (keys with create/delete), recommendations, webhooks, multistream, privacy, backup, branding, badges, languages, advanced, about - Player with stats overlay, keyboard shortcuts, quality selector - i18n (7 locales via `@streamplace/i18n`) @@ -22,34 +23,21 @@ The existing React Native app (`js/app`) is the source of truth. The new web app From `blueskySlice.ts` header comments: -- `createStreamKeyRecord` - throws - `createLivestreamRecord` - throws - `updateLivestreamRecord` - throws - `golivePost` - throws -- `createBlockRecord` - throws +- `createBlockRecord` - throws (low priority, not in RN either) ### Web App: Missing Entirely - Go-live flow (no route, no UI, no broadcaster backend) -- Stream key creation (keys.tsx is read-only delete) - Following feed (sidebar "Following" is disabled) - Notifications surface (no route, no header bell) -- Search (no route) -- Chat popout route (`/chat-popout/:handle` linked but no route file) -- Upload VOD -- Multi-stream viewer -- Embed screens (stream, VOD, info widget, danmu OBS) +- Multi-stream viewer (grid of multiple live streams; `dashboard/multistream/` is for RTMP target management, not viewing) - Popout windows (livestream, multistream, stream monitor, info widget) - Download screen - Support screen -- "What is Streamplace?" about screen -- Error boundary -- Suspense fallback -- Theme provider (useColorScheme is hardcoded "dark") -- TooltipProvider (not mounted globally) -- FullscreenProvider (declared but not mounted) -- VideoElementProvider (declared but not mounted) -- React Query (all fetching is hand-rolled useState/useEffect) +- "What is Streamplace?" about screen (web has `/settings/about` but that's settings-scoped, not a public landing page) ### RN App: Feature Inventory @@ -81,6 +69,45 @@ All screens in `js/app/src/screens/`: | `support.tsx` | Support page | | `app-return.tsx` | OAuth return handling | +### Screen-by-Screen Comparison + +| RN Screen | Web Equivalent | Status | Notes | +| --------------------------- | ------------------------------ | ---------- | ---------------------------------------------------------------------------------------- | +| `home.tsx` | `$user/index.tsx`, `index.tsx` | ✅ Parity | Web has infinite scroll instead of pull-to-refresh. Both show live grid + viewer counts. | +| `mobile-stream.tsx` | `$user/index.tsx` | ⚠️ Partial | Web is desktop-focused. No dedicated mobile-optimized live view. | +| `mobile-go-live.tsx` | — | ❌ Missing | Mobile camera ingest, no web equivalent. | +| `launch-go-live.tsx` | — | ❌ Missing | "Ready to go live?" prompt, no web equivalent. | +| `live-dashboard.tsx` | `dashboard/stream/index.tsx` | ✅ Parity | Web has additional metadata/moderation sections. | +| `video-list.tsx` | `videos.tsx` | ✅ Parity | Both use same API, infinite scroll, responsive grid. | +| `video.tsx` | `$user/video/$tid.tsx` | ✅ Parity | Single VOD playback. | +| `vod.tsx` | `$user/video/$tid.tsx` | ✅ Merged | RN has separate vod.tsx and video.tsx; web combines into one route. | +| `upload.tsx` | `dashboard/upload.tsx` | ✅ Done | tus upload, metadata form, thumbnail, progress. In creator dashboard. | +| `chat-popout.tsx` | `chat-popout.$user.tsx` | ✅ Done | Standalone chat window. | +| `multi.tsx` | — | ❌ Missing | Grid of multiple simultaneous live streams. Not the same as dashboard/multistream. | +| `about.tsx` | `settings/about.tsx` | ⚠️ Partial | Web version is settings-scoped. RN has a public "What is Streamplace?" page. | +| `download.tsx` | — | ❌ Missing | App download/promo page with store links. | +| `embed.tsx` | `embed/$user/index.tsx` | ✅ Done | Minimal-chrome stream embed. | +| `vod-embed.tsx` | `embed/$user/video/$tid.tsx` | ✅ Done | VOD embed. | +| `danmu-obs.tsx` | `embed/danmu-obs/$user.tsx` | ✅ Done | Danmu OBS overlay, full feature parity. | +| `info-widget-embed.tsx` | `embed/info-widget/$user.tsx` | ✅ Done | Info widget embed. | +| `popout-livestream.tsx` | — | ❌ Missing | Popout livestream player window. | +| `popout-multistream.tsx` | — | ❌ Missing | Popout multistream window. | +| `popout-stream-monitor.tsx` | — | ❌ Missing | Popout stream monitor. | +| `popout-info-widget.tsx` | — | ❌ Missing | Covered by `/embed/info-widget/$user` but no dedicated popout route. | +| `support.tsx` | — | ❌ Missing | Redirects to Google Form. | +| `app-return.tsx` | Handled by `login.tsx` | ✅ Done | OAuth callback handled differently on web. | + +### Web Features Beyond RN Parity + +| Feature | Status | Notes | +| ---------------------------------- | ------- | ------------------------------------------------------------ | +| Search (header bar + popover) | ✅ Done | RN has no equivalent search UI. | +| Danmu overlay | ✅ Done | Bullet comments with lane assignment, mobile parity. | +| Theatre mode | ✅ Done | Sidebar + header hidden, `T` shortcut. RN has no equivalent. | +| Picture-in-Picture | ✅ Done | Web-native PiP API. RN has no equivalent. | +| Dashboard webhooks/branding/badges | ✅ Done | Advanced settings panels not in RN. | +| Keyboard shortcuts | ✅ Done | Space, m, f, T shortcuts. RN has no equivalent. | + ### RN App: Navigation Tree ``` @@ -183,6 +210,28 @@ RootStack | `expo-localization` | Locale detection | navigator.language / Intl | | `expo-system-ui` | System UI | N/A | +### API Usage: Web-Only vs RN-Only + +**APIs used in web but not RN:** + +- `agent.place.stream.multistream.listTargets` / `putTarget` / `createTarget` / `deleteTarget` (RTMP target management) +- `agent.place.stream.server.listWebhooks` / `updateWebhook` / `createWebhook` / `deleteWebhook` +- `agent.place.stream.badge.def.list` / `def.create` / `issuance.create` (badge issuer) +- `agent.place.stream.branding.updateBlob` / `deleteBlob` +- `agent.place.stream.server.getStorage` / `upsertStorage` + +**APIs used in RN but not web:** + +- `agent.place.stream.media.getUploadStatus` / `createUpload` / `publishVideo` (upload flow) +- Grid-based multi-stream viewing (not RTMP management) + +**Shared APIs (both use):** + +- `agent.place.stream.live.getLiveUsers` (home page) +- `agent.place.stream.live.searchActorsTypeahead` (search/recommendations) +- `agent.place.stream.media.getVideoList` (video listings) +- Chat APIs (messages, pinned comments, mentions) + --- ## The Plan @@ -327,15 +376,11 @@ These don't need to be full routes. They can be: The functionality should be accessible without cluttering the route tree. -#### 2l. VOD Download - -Already covered in 2d. (Duplicate removed.) - --- ### Phase 3: Go-Live & Broadcaster Flow -The biggest gap. This is deferred because the approach is still being thought through. The RN app has a complete go-live flow; the web app has zero. +The biggest gap. The RN app has a complete go-live flow; the web app has zero. This phase covers both OBS-based (RTMP) and browser-based (WebRTC) broadcasting. #### 3a. Stream Key Creation @@ -343,10 +388,12 @@ Un-stub `createStreamKeyRecord` in `blueskySlice.ts`. Add `@atproto/crypto` + `v #### 3b. Go-Live Route & UI -New route: `/go-live` (or `/settings/go-live`) +New route: `/go-live` "Start streaming" button in sidebar (replace disabled Following with Go Live when logged in). Stream key display + copy-to-clipboard. OBS/Restream setup instructions. RTMP URL + stream key display. +This covers both `launch-go-live.tsx` (the "Ready to go live?" prompt) and the go-live entry point from the RN app. + #### 3c. Live Dashboard New route: `/$user/dashboard` (broadcaster view) @@ -369,6 +416,8 @@ The RN app uses `BentoGrid` from `@streamplace/components` which is RN-specific. This is significant work and only matters for a subset of users who want browser-based broadcasting. Could be deferred to after initial go-live if OBS-only (RTMP key + instructions) is sufficient. +Note: The RN app has `mobile-go-live.tsx` for mobile camera ingest. On web, this could be a simpler route or part of the go-live flow using `getUserMedia()` + WebRTC, which works on mobile browsers. + #### 3e. Following Feed Sidebar "Following" link → working route. Fetch following list from PDS, filter live users to followed accounts, display as StreamCard grid (same as home, filtered). @@ -381,31 +430,35 @@ This may need server-side work depending on how following data is fetched, plus These go beyond parity to make the web app genuinely better than the RN app on desktop. -#### 4a. Picture-in-Picture +#### 4a. Picture-in-Picture ✅ `video.requestPictureInPicture()` API. PiP button in player controls. Continue watching while browsing other tabs. -#### 4b. Web Share API +#### 4b. Theatre Mode ✅ + +Sidebar + header hidden, video fills viewport. Toggle button in player controls, `T` keyboard shortcut. State persisted to localStorage. + +#### 4c. Web Share API Share button on streams and VODs. `navigator.share({ title, url, text })`. Fallback to copy-to-clipboard. -#### 4c. Keyboard Shortcuts +#### 4d. Keyboard Shortcuts -Global shortcuts: `G` for go-live, `/` for search, `?` for help. Player shortcuts: already done (space, m, f). Chat shortcuts: `Enter` to send, `Escape` to close. +Global shortcuts: `G` for go-live, `/` for search, `?` for help. Player shortcuts: already done (space, m, f, T). Chat shortcuts: `Enter` to send, `Escape` to close. -#### 4d. Multi-Window / Pop-Out Player +#### 4e. Multi-Window / Pop-Out Player Pop-out player to separate browser window. `window.open()` with minimal player UI. Sync play/pause state between windows. -#### 4e. URL-Based Deep Linking +#### 4f. URL-Based Deep Linking ✅ Every view has a stable URL (already done with TanStack Router). Shareable links to specific moments in VODs. Embeddable via query params. -#### 4f. Theming +#### 4g. Theming ✅ Full light mode + dark mode implementation. `prefers-color-scheme` media query listener. Manual toggle in settings. CSS variables already partially defined in `styles.css`. -#### 4g. PWA (Progressive Web App) +#### 4h. PWA (Progressive Web App) `manifest.json` with app name, icons, theme color. Service worker for offline shell caching. "Add to Home Screen" prompt. Offline fallback page. @@ -460,18 +513,18 @@ Full light mode + dark mode implementation. `prefers-color-scheme` media query l ### Phase 2: Feature Parity — In Progress -| Item | Status | Notes | -| -------------------------- | ------- | ------------------------------------------------------------------------------------------------ | -| 2a. Chat popout | Done | `/chat-popout/$user` route, standalone window, WebSocket chat. | -| 2b. Block/mute in chat | Pending | `createBlockRecord` still stubbed. UI buttons not wired. | -| 2c. Chat profile edit | Done | Inline editor with color picker, avatar, badges. | -| 2d. VOD download | Done | Fetch + Blob + createObjectURL + anchor download. | -| 2e. Multi-stream viewer | Pending | Not started. | -| 2f. Search | Done | Inline header bar with popover results. | -| 2g. Upload VOD | Pending | Not started. | -| 2h. Embed screens | Done | `/embed/$user`, `/embed/$user/video/$tid`, `/embed/info-widget/$user`, `/embed/danmu-obs/$user`. | -| 2i. Popout windows | Pending | Not started. | -| 2k. About/Download/Support | Pending | Not started. | +| Item | Status | Notes | +| -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ | +| 2a. Chat popout | Done | `/chat-popout/$user` route, standalone window, WebSocket chat. | +| 2b. Block/mute in chat | Pending | `createBlockRecord` still stubbed. UI buttons not wired. | +| 2c. Chat profile edit | Done | Inline editor with color picker, avatar, badges. | +| 2d. VOD download | Done | Fetch + Blob + createObjectURL + anchor download. | +| 2e. Multi-stream viewer | Pending | Not started. `multi.tsx` in RN shows grid of concurrent streams; web has nothing for this. | +| 2f. Search | Done | Inline header bar with popover results. | +| 2g. Upload VOD | Done | `/dashboard/upload` route with tus upload, metadata form, thumbnail, progress, video management. Moved into creator dashboard. | +| 2h. Embed screens | Done | `/embed/$user`, `/embed/$user/video/$tid`, `/embed/info-widget/$user`, `/embed/danmu-obs/$user`. | +| 2i. Popout windows | Pending | Missing: popout-livestream, popout-multistream, popout-stream-monitor, popout-info-widget. | +| 2k. About/Download/Support | Pending | About: web has settings/about but no public landing page. Download + Support completely missing. | ### Additional Work Done (not in original plan) @@ -479,59 +532,27 @@ Full light mode + dark mode implementation. `prefers-color-scheme` media query l - Danmu (bullet comments) overlay with mobile-parity implementation - Danmu toggle persisted to localStorage, standalone button in player controls - Migrated fetchLiveUsers from zustand to React Query +- VOD upload with tus-js-client, metadata form, thumbnail, progress tracking -### Phase 3: Go-Live — Not Started +### Phase 3: Go-Live — In Progress -### Phase 4: Web-Native Enhancements — In Progress - -| Item | Status | Notes | -| ---------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 4a. Picture-in-Picture | Done | PiP toggle button in player controls. Uses `requestPictureInPicture()` API. Support detection, state tracking via events. | -| 4b. Theatre mode | Done | Sidebar + header hidden, video fills viewport. Toggle button in player controls, `T` keyboard shortcut. State persisted to localStorage. Works on both live streams and VODs. | +| Item | Status | Notes | +| ----------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | +| 3a. Stream Key Creation | Done | `createStreamKeyRecord` in blueskySlice with @atproto/crypto + viem. Key list + create in Stream Settings. | +| 3b. Go-Live Route & UI | Pending | Not started. | +| 3c. Live Dashboard | Pending | Not started. | +| 3d. WebRTC Publishing | Pending | Deferred. | +| 3e. Following Feed | Pending | Not started. | ---- +### Phase 4: Web-Native Enhancements — In Progress -## Suggested Priority Order - -### Phase 1: Foundation (Do First) - -1. React Query setup + incremental migration (1a) -2. Mount missing providers: Tooltip, Fullscreen, VideoElement (1d) -3. Error boundary (1b) -4. Suspense fallbacks (1c) -5. Theme provider plumbing (1e) -6. i18n audit (1f) -7. Sentry (1g) -8. Code review & cleanup (1h) - -### Phase 2: Feature Parity (Do Second) - -1. Chat popout (2a) -2. Block/mute in chat (2b) -3. Chat profile edit (2c) -4. VOD download (2d) -5. Search (2f) -6. Multi-stream viewer (2e) -7. Upload VOD (2g) -8. Embed screens (2h) -9. About/Download/Support accessible somewhere (2k) -10. Popout windows (2i) - -### Phase 3: Go-Live (Do Last) - -1. Stream key creation (3a) -2. Go-live route + sidebar button (3b) -3. Live dashboard with movable widgets (3c) -4. WebRTC publishing (3d) -5. Following feed (3e) - -### Phase 4: Web-Native Enhancements (Nice-to-Have) - -1. Picture-in-Picture (4a) — Done -2. Theatre mode — Done -3. Web Share API (4b) -4. Keyboard shortcuts (4c) -5. Multi-window player (4d) -6. Deep linking (4e) -7. Theming (4f) -8. PWA (4g) +| Item | Status | Notes | +| -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 4a. Picture-in-Picture | Done | PiP toggle button in player controls. Uses `requestPictureInPicture()` API. Support detection, state tracking via events. | +| 4b. Theatre mode | Done | Sidebar + header hidden, video fills viewport. Toggle button in player controls, `T` keyboard shortcut. State persisted to localStorage. Works on both live streams and VODs. | +| 4c. Web Share API | Pending | Not started. | +| 4d. Keyboard Shortcuts | Partial | Player shortcuts done (space, m, f, T). Global shortcuts (`G`, `/`, `?`) not implemented. | +| 4e. Multi-Window/Pop-Out | Pending | Not started. | +| 4f. URL-Based Deep Linking | Done | TanStack Router provides stable URLs for all routes. | +| 4g. Theming | Done | Media query listener, manual toggle, localStorage persistence. | +| 4h. PWA | Pending | Not started. | diff --git a/js/web/public/locales/en-US/common.json b/js/web/public/locales/en-US/common.json index 0e3520e9f..f3bf7c046 100644 --- a/js/web/public/locales/en-US/common.json +++ b/js/web/public/locales/en-US/common.json @@ -73,6 +73,7 @@ "chat-send-message": "Send a message", "chat-replying-to": "Replying to {$handle}", "chat-insert-emoji": "Insert emoji", + "chat-skin-tone": "Skin tone", "chat-send-button": "Chat", "chat-failed-send": "Failed to send message", "chat-log-in-to": "Log in to chat", @@ -136,5 +137,85 @@ "no-results-for": "No results for \"{$query}\"", "find-streamers": "Find streamers by name or handle", "something-went-wrong": "Something went wrong", - "unexpected-error": "An unexpected error occurred." + "unexpected-error": "An unexpected error occurred.", + "upload-add-tag": "Add tag, press Enter", + "upload-add-thumbnail": "Add thumbnail image", + "upload-cancel": "Cancel", + "upload-choose-file": "Choose a video file", + "upload-choose-file-btn": "Choose file", + "upload-content-warnings": "Content Warnings", + "upload-delete": "Delete", + "upload-deleting": "Deleting...", + "upload-description": "Description", + "upload-description-placeholder": "Describe your video...", + "upload-details": "Details", + "upload-file": "File", + "upload-license": "License", + "upload-optional-info": "Optional · JPG, PNG up to 975KB", + "upload-preparing": "Preparing upload...", + "upload-processing": "Processing video", + "upload-publish": "Publish", + "upload-published": "Published", + "upload-publishing": "Publishing...", + "upload-publishing-btn": "Publishing...", + "upload-ready-publish": "Ready to publish", + "upload-status": "Status", + "upload-tags": "Tags", + "upload-thumbnail": "Thumbnail", + "upload-thumbnail-preview": "Thumbnail preview", + "upload-title": "Title", + "upload-title-placeholder": "Give your video a title", + "upload-update-video": "Update Video", + "upload-updating": "Updating...", + "upload-view-video": "View video →", + "upload-waiting-process": "Waiting to process...", + "create-key-description-delete-if-exposed": "If you think your stream key has been exposed, delete it immediately and create a new one!", + "create-key-description-staff": "Staff will never, ever ask for your stream key!", + "create-key-do-not-share": "DO NOT SHARE THIS KEY OR SHOW IT ON STREAM, as ANYONE with this key may be able to stream from your account!", + "create-key-title": "Are you sure about creating a new stream key?", + "create-stream-key": "Create Stream Key", + "stream-key": "Stream Key", + "stream-key-help": "Your stream key identifies you to Streamplace. You can have more than one stream key at a time, and it's a good idea to prune old keys that you aren't using via the key manager.", + "stream-key-info-description": "For security reasons, you won't be able to view it again through your account. If you lose this stream key, you'll need to generate a new one.", + "stream-key-label": "Stream Key", + "stream-key-save-now": "Please save this stream key somewhere safe.", + "allow-everyone-archive": "Allow everyone to archive your content indefinitely", + "allow-everyone-distribute": "Allow everyone to distribute your content", + "allowed-broadcasters": "Allowed Broadcasters", + "allowed-broadcasters-help": "Enter the DIDs of the broadcasters you want to allow, one per line.", + "content-rights": "Content Rights", + "content-rights-help": "Optional copyright and license information for your stream.", + "content-warnings": "Content Warnings", + "content-warnings-help": "You're required to flag your stream with themes that viewers may want a heads-up about.", + "copied-to-clipboard": "Copied to clipboard", + "copy-link": "Copy link", + "copyright-notice": "Copyright Notice", + "copyright-year": "Copyright Year", + "credit-line": "Credit Line", + "custom-license": "Custom…", + "custom-license-url": "Custom License URL/Text", + "delete-after": "Delete After", + "delete-after-help": "Duration in seconds (e.g. 300 for 5 minutes).", + "distribution": "Distribution", + "distribution-help": "Control who can redistribute your stream and for how long archives are kept.", + "editing-video": "Editing video", + "failed-to-create-key": "Failed to create key", + "go-to-video": "Go to video", + "key-manager": "Key Manager", + "license": "License", + "login-to-manage-videos": "Please log in to manage your videos.", + "login-to-upload": "Please log in to upload videos.", + "metadata": "Metadata", + "metadata-learn-more": "Learn more about content metadata", + "metadata-save-failed": "Failed to save metadata", + "metadata-saved": "Metadata saved", + "moderation": "Moderation", + "moderation-coming-soon": "Moderator management hooks aren't on the web yet — this section is a placeholder.", + "moderation-help": "Add/remove stream moderators. Moderators can hide chat messages and time out users in your chat.", + "nav-dashboard": "Dashboard", + "nav-settings": "Settings", + "no-videos-yet-upload": "No videos yet.", + "optional": "Optional", + "required": "Required", + "select-license": "Select a license…" } diff --git a/js/web/public/locales/en-US/settings.json b/js/web/public/locales/en-US/settings.json index 7a21952c0..457536511 100644 --- a/js/web/public/locales/en-US/settings.json +++ b/js/web/public/locales/en-US/settings.json @@ -251,5 +251,22 @@ "chat-profile-preview-message": "Hello, world!", "chat-profile-self-labels": "Self Labels", "chat-profile-label-bot": "Bot", - "chat-profile-label-bot-description": "Mark your chat messages as coming from a bot." + "chat-profile-label-bot-description": "Mark your chat messages as coming from a bot.", + "control-panel": "Control Panel", + "create-stream-key-in-stream-settings": "Create a stream key in Stream Settings", + "creator-settings": "Creator Settings", + "delete-all-keys": "Delete All Keys", + "delete-all-keys-btn": "Delete All", + "delete-all-keys-description": "This will permanently remove all {$count} stream keys. Any streaming software configured with these keys will stop working.", + "delete-all-keys-title": "Delete all stream keys?", + "failed-delete-keys": "Failed to delete {$count} key(s)", + "failed-to-delete-key": "Failed to delete key", + "my-videos": "My Videos", + "stream-settings": "Stream Settings", + "stream-settings-link": "Stream Settings", + "time-ago-days": "{$days}d ago", + "time-ago-hours": "{$hours}h ago", + "time-ago-minutes": "{$minutes}m ago", + "time-ago-seconds": "{$seconds}s ago", + "upload-videos": "Upload Videos" } diff --git a/js/web/src/components/dashboard/dashboard-chrome.tsx b/js/web/src/components/dashboard/dashboard-chrome.tsx index b7ae00793..1dded0a1f 100644 --- a/js/web/src/components/dashboard/dashboard-chrome.tsx +++ b/js/web/src/components/dashboard/dashboard-chrome.tsx @@ -4,11 +4,13 @@ import { useSession } from "@/lib/session"; import { Link, Outlet } from "@tanstack/react-router"; import { ArrowLeft, + ArrowUpFromLine, Key, LayoutGrid, ListVideo, Radio, Share2, + Video, Webhook, } from "lucide-react"; import { useState } from "react"; @@ -38,6 +40,16 @@ interface NavLink { /** Main nav — appears above the Settings group. */ const MAIN_NAV: NavLink[] = [ { to: "/dashboard", icon: LayoutGrid, labelKey: "control-panel" }, + { + to: "/dashboard/upload", + icon: ArrowUpFromLine, + labelKey: "upload-videos", + }, + { + to: "/dashboard/videos", + icon: Video, + labelKey: "my-videos", + }, ]; /** Settings group — appears under a "Settings" header. */ diff --git a/js/web/src/components/dashboard/stream-info.tsx b/js/web/src/components/dashboard/stream-info.tsx index f38ec666d..575c4420e 100644 --- a/js/web/src/components/dashboard/stream-info.tsx +++ b/js/web/src/components/dashboard/stream-info.tsx @@ -15,7 +15,7 @@ import { import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import type { PlaceStreamLivestream } from "streamplace"; +import { place } from "streamplace"; import { useStore } from "zustand"; import { usePDSAgent, @@ -75,7 +75,7 @@ export function StreamInfoWidget({ store }: { store: LivestreamStore }) { // Form state const [title, setTitle] = useState(""); const [activity, setActivity] = useState< - PlaceStreamLivestream.Record["activity"] | undefined + place.stream.livestream.Main["activity"] | undefined >(undefined); const [tags, setTags] = useState([]); const [tagInput, setTagInput] = useState(""); @@ -96,7 +96,7 @@ export function StreamInfoWidget({ store }: { store: LivestreamStore }) { if (livestream.record.title) setTitle(livestream.record.title); if (livestream.record.activity) { setActivity( - livestream.record.activity as PlaceStreamLivestream.Record["activity"], + livestream.record.activity as place.stream.livestream.Main["activity"], ); } if (livestream.record.tags) setTags(livestream.record.tags as string[]); @@ -170,23 +170,23 @@ export function StreamInfoWidget({ store }: { store: LivestreamStore }) { setLoading(true); try { - const record: PlaceStreamLivestream.Record = { + const record = { $type: "place.stream.livestream", title: title.trim(), - url: `${url}/${agent.did}`, - createdAt: new Date().toISOString(), - lastSeenAt: new Date().toISOString(), + url: `${url}/${agent.did}` as any, + createdAt: new Date().toISOString() as any, + lastSeenAt: new Date().toISOString() as any, idleTimeoutSeconds: idleTimeout ? 300 : 0, activity, tags: tags.length > 0 ? tags : undefined, - }; + } as any; // Upload thumbnail if provided if (thumbnail) { try { const uploaded = await agent.uploadBlob(thumbnail); if (uploaded.success) { - record.thumb = uploaded.data.blob; + (record as any).thumb = uploaded.data.blob; } } catch (e) { console.error("Thumbnail upload failed:", e); @@ -195,12 +195,11 @@ export function StreamInfoWidget({ store }: { store: LivestreamStore }) { if (!hasLivestream || isEnded) { // Create new livestream - const result = await agent.place.stream.live.startLivestream({ + const result = await agent.client.call(place.stream.live.startLivestream, { livestream: record, - streamer: agent.did, + streamer: agent.did as any, createBlueskyPost: createPost, }); - if (!result.success) throw new Error("Failed to start livestream"); toast.success( t("livestream-announced", { defaultValue: "Livestream announced" }), ); @@ -259,7 +258,7 @@ export function StreamInfoWidget({ store }: { store: LivestreamStore }) { if (!agent || !canEnd) return; setEndingLivestream(true); try { - await agent.place.stream.live.stopLivestream({}); + await agent.client.call(place.stream.live.stopLivestream, {}); toast.success( t("livestream-ended", { defaultValue: "Livestream ended" }), ); @@ -383,7 +382,7 @@ export function StreamInfoWidget({ store }: { store: LivestreamStore }) { } }} placeholder={t("add-tag", { defaultValue: "Add tag…" })} - className="min-w-[80px] flex-1 rounded-full px-2 py-0 text-sm" + className="min-w-20 flex-1 rounded-full px-2 py-0 text-sm" /> )} {/* Language picker */} diff --git a/js/web/src/components/dashboard/upload-form.tsx b/js/web/src/components/dashboard/upload-form.tsx new file mode 100644 index 000000000..9d9cfc079 --- /dev/null +++ b/js/web/src/components/dashboard/upload-form.tsx @@ -0,0 +1,439 @@ +// Reusable upload form: file picker, metadata, thumbnail, progress, actions. +// Used by both /dashboard/upload (new upload) and /dashboard/videos (edit). +import { Button } from "@/components/ui/button"; +import { humanBytes, type useUpload } from "@/hooks/use-upload"; +import { LICENSE_OPTIONS } from "@/lib/content-licenses"; +import { CONTENT_WARNINGS } from "@/lib/content-warnings"; +import { Link } from "@tanstack/react-router"; +import { + AlertCircle, + ArrowUp, + CheckCircle2, + ImagePlus, + LoaderCircle, + Video, + X, +} from "lucide-react"; +import { useCallback, useRef } from "react"; + +type UploadHook = ReturnType; + +interface UploadFormProps { + /** The upload hook instance. The parent owns this so it can pre-fill state for edits. */ + upload: UploadHook; + /** When set, the form is in "edit" mode: hides file picker, shows update/delete actions. */ + editingVideoUri?: string; + /** Called when the user wants to cancel editing. */ + onCancelEdit?: () => void; + /** Called when the user clicks "Update Video". Receives the hook instance so the parent can drive the API call. */ + onUpdate?: (upload: UploadHook) => Promise; + /** Called when the user clicks "Delete Video". */ + onDelete?: () => Promise; + /** Whether an update or delete is in progress (disables buttons). */ + isUpdating?: boolean; + isDeleting?: boolean; +} + +export function UploadForm({ + upload, + editingVideoUri, + onCancelEdit, + onUpdate, + onDelete, + isUpdating, + isDeleting, +}: UploadFormProps) { + const fileInputRef = useRef(null); + const thumbnailInputRef = useRef(null); + + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + const f = e.target.files?.[0] ?? null; + upload.selectFile(f); + e.target.value = ""; + }, + [upload], + ); + + const handleThumbnailChange = useCallback( + (e: React.ChangeEvent) => { + const f = e.target.files?.[0] ?? null; + upload.selectThumbnail(f); + e.target.value = ""; + }, + [upload], + ); + + const isEdit = !!editingVideoUri; + + return ( +
+ {/* ── left column: metadata ────────────────────────────────────── */} +
+ {/* title + description */} +
+

+ Details +

+
+
+ + upload.setTitle(e.target.value)} + maxLength={140} + placeholder="Give your video a title" + className="w-full rounded-md border border-(--color-border) bg-(--color-bg) px-3 py-2 text-sm text-(--color-fg) placeholder:text-(--color-fg-muted)" + /> +
+
+
+ +