bitvid attached every hosted source with `videoEl.src = url`, which only plays
HLS on Safari-family browsers. Firefox 146 fails with MEDIA_ERR_SRC_NOT_SUPPORTED
(canPlayType returns ""), so every .m3u8 note -- e.g. anything published by
nostube/slidestr -- was unplayable there and the URL-health badge showed
"offline". Chrome/Brave 149 happen to play HLS natively, which is why this
looked browser-specific.
Vendor hls.js (npm run build:hls) and route HLS sources through it:
- js/services/hlsPlayback.js owns detection, engine choice and the hls.js
lifecycle. Engine choice keys off MediaSource, NOT canPlayType: Chrome
returns "maybe" for every HLS type, so a native-first branch would hang.
Native is used only when MSE is genuinely unavailable.
- The bundle is the FULL build, not hls.js/light -- light drops the alternate
audio stream controller and real ladders demux audio into an EXT-X-MEDIA
group. It is lazy-imported only when a .m3u8 is encountered, so ordinary
MP4/WebTorrent playback never pays the ~500KB.
- modalCoordinator tears the instance down first; clearing `src` alone leaves
the transmuxing worker and segment fetches running after the modal closes.
- urlHealthController probes HLS by fetching the playlist for #EXTM3U. The old
bare-<video> probe could not load it and on Chrome emitted no events at all,
burning the full timeout on every HLS card.
- HLS_PLAYBACK_START_TIMEOUT widens the 3s start window, which fired
mid-startup: the bundle, master playlist, variant playlist, init segment and
first media segment are four-plus serial round trips.
Also fixes two bugs found while verifying this:
- A stale MediaError was reported as the current failure. Teardown clears `src`
and calls load(), leaving MEDIA_ERR_SRC_NOT_SUPPORTED ("Empty src attribute")
on the element; a later watchdog fallback rendered that as "Hosted playback
failed: source not supported or blocked." over a stream that was playing
fine. Only a MediaError produced by the current attempt is surfaced now.
Sources are also bound before the watchdogs are armed, so hls.js's own
attachMedia() src swap cannot read as a playback failure.
- `audio/x-mpegurl` is a legacy alias for an HLS playlist of any kind, not an
audio-only assertion. isAudioOnlyVideoObject misfiled HLS video ladders
published with it as podcasts and dropped them from every video feed.
Verified end-to-end in Chromium, Chrome 149, Brave 149 and Firefox 146 against
a live HLS stream: Firefox goes from "No playable source found." to playing.
Rollback: stop calling attachHlsSource() and the previous `src` assignment
returns; see docs/playback-fallback.md.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The login modal now shows <bitlogin-auth> -- a portable Nostr identity
behind a login name and password -- as the default sign-in path,
styled with bitvid's own brand tokens (accent-strong, surfaces, radii).
The existing NIP-07 extension, nsec, and NIP-46 remote-signer options
still work exactly as before, just collapsed under a new "Other login
options" fold instead of a flat "Available methods" grid; nip07 no
longer carries the stale "Recommended" eyebrow now that BitLogin has
that role.
BitLogin isn't a single button-click-then-await-a-promise flow like
the other providers -- it's a whole multi-screen sign-in/create/recover
UI -- so it gets a permanent, statically-declared widget in
components/login-modal.html (js/ui/bitloginModalIntegration.js relocates
it, never destroys and recreates it, since removal tears down its crypto
Web Worker) instead of a grid button. It still goes through the exact
same session-establishment path as every other signer: a new "bitlogin"
provider (js/services/authProviders/bitlogin.js) lets AuthService
resolve and activate it via the normal requestLogin -> handleAuthLogin
path, while providersForModal excludes it from the grid so it isn't
rendered twice.
Two things this needed beyond the wiring itself:
- js/app.js is one line from its lint/file-size ceiling, so the only
change there is renaming an import binding (providers -> providersForModal),
a net-zero line change. The actual widget/event wiring lives in a new,
unconstrained module instead.
- The login modal's HTML (including the permanent widget element) loads
asynchronously after bootstrap runs, so wireBitloginLogin() waits via
MutationObserver rather than assuming a specific call order.
Vendored via scripts/build-bitlogin-widget.mjs (mirrors the existing
Bitcoin Connect/Blossom vendor steps): BitLogin isn't published to npm,
so the widget build is copied verbatim from the sibling ../bitlogin
checkout into committed vendor/bitlogin/, then lazy-imported (like
Bitcoin Connect) so its ~350KB never weighs on visitors who never open
the login modal.
Gated behind FEATURE_BITLOGIN (config/instance-config.js), default on
for this unstable-branch experiment per AGENTS.md §1 -- reset to false
before this reaches main until verified end-to-end against production
relays.
Verified: npm run lint (clean), the full auth/login test suite (43/43,
including new coverage for the provider + adapter), and a manual
Chrome pass against the built dist/ -- widget renders themed correctly,
"Other login options" still shows the original four methods unchanged.
Blossom servers reject a .torrent (415), so a Blossom video's magnet is webseed-only and can't bootstrap WebTorrent. This restores P2P without hosting a .torrent:
- Upload hardening: correct Content-Type (overrides empty/generic application/octet-stream), HTTP status surfaced in errors, and best-effort .torrent sidecar so a rejected sidecar no longer sinks the upload (URL-first fallback).
- Publish: when the .torrent can't be hosted, publish the piece-map as an infohash-keyed companion event (kind 30078, d=bitvid:torrent:<infohash>) and ship a ws= magnet. Gated by FEATURE_BLOSSOM_TORRENT_METADATA (default off = no trace); size-capped.
- Playback: torrentMetadataService lazily fetches the companion, VERIFIES the reconstructed infohash === the magnet's btih (untrusted content), sources the webseed from the video event, and hands the .torrent buffer to WebTorrent so it streams with zero peers. Relay reads go through the subscription manager.
Inherited by any upload-based media (shorts, audio-publish); excludes ingest/live. See docs/blossom-torrent-metadata-plan.md. Includes a Prettier reflow in css/tailwind.source.css.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 0 of docs/blossom-plan.md. No UX; off-by-default ("off = no trace").
- Vendored: pinned [email protected] bundled by scripts/build-blossom-sdk.mjs
→ vendor/blossom-client-sdk.bundle.min.js (esbuild, browser ESM, ~27KB). The
optional @cashu/cashu-ts peer dep (BUD-07 payments, only reached on a lazy 402
branch bitvid never hits) is aliased to a throwing stub. `npm run build:blossom-sdk`.
- Flag: FEATURE_BLOSSOM_STORAGE wired like FEATURE_AUDIO_INGEST / _BITCOIN_CONNECT
(instance-config → config → constants), default FALSE.
- Service: js/services/blossomService.js implements the same
uploadFile({…}) → {url,key} contract as s3UploadService (so a later
isBlossomProvider branch drops into mediaUploader). Lazy-loads the SDK; core
path = createUploadAuth(signer, sha256) + multiServerUpload (BUD-11 auth via
bitvid's existing signer, BUD-04 mirror). Torrent parity + result-shape mapping +
mediaUploader wiring + Storage-pane config land in Phase 1.
Tests: tests/blossom-service.test.mjs (5 — availability gate + input guards).
Lint + build clean. Nothing wired into the app yet.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 0+1 of docs/bitcoin-connect-plan.md (#61). Adds Alby's Bitcoin Connect as a
one-tap NWC connect front-end that feeds bitvid's EXISTING wallet pipeline; manual
URI entry stays as the backup.
- Vendored: pinned @getalby/[email protected].3 bundled by
scripts/build-bitcoin-connect.mjs → vendor/bitcoin-connect.bundle.min.js (esbuild,
browser ESM). crypto-shim aliases the L402-only `import('crypto')` bitvid never
calls. `npm run build:bitcoin-connect` rebuilds it.
- Flag: FEATURE_BITCOIN_CONNECT wired like FEATURE_AUDIO_INGEST
(instance-config → config → constants).
- UI: a "Connect wallet" button in the Wallet Connect pane (hidden unless the flag
is on); the manual URI field stays below as the backup.
- Controller: ProfileWalletController.handleBitcoinConnect() lazy-imports the bundle
(only on click — the ~700KB never touches the main bundle), inits NWC-only with
least-privilege requestMethods and persistConnection:false, then on onConnected
extracts provider.client.nostrWalletConnectUrl → the SAME validate→save path as
the manual field → disconnect() so bitvid solely holds the spending secret. The
URI is never logged.
NOTE: config/instance-config.js FEATURE_BITCOIN_CONNECT is TRUE for local testing
only. Reset to false (shipping default) before pushing/deploying. Local commit;
not pushed, not deployed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds the Audio/Music/Podcast feature flag to the instance config so a deployer
enables/disables it in one place like the other flags. Follows the exact
FEATURE_NIP71_INGEST wiring: export in config/instance-config.js → re-export in
js/config.js → DEFAULT_FLAGS + runtimeFlags snapshot + `export let` in
js/constants.js. Defaults FALSE ("off = no trace") — only an explicit `true`
lights it up; with it off nothing audio renders and the shipped audio guard keeps
audio-only notes out of the video feeds. No consumers yet (build only).
Also locks docs/audio-integration-plan.md decisions D1–D10 to the agreed
recommendations (status → DECISIONS LOCKED).
Verified: constants + config re-export FEATURE_AUDIO_INGEST === false by default.
Lint + build clean. docs/audio-integration-plan.md, TODO #60.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Turns on the admin Submissions tab now that the loop is complete (schema +
facade + admin approve/deny + forms). The forms already publish kind-30083
unconditionally, so enabling the tab is what makes the new intake coherent —
submissions are now visible + actionable instead of landing with no admin UI.
Rollback: set false to hide the tab (forms keep publishing 30083).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First slice of structured submissions (replacing the kind-4 encrypted DM forms).
Pure, hermetic core — no UI or network yet.
- nostrEventSchemas.js: SUBMISSION_KIND=30083 (+ NOTE_TYPES.SUBMISSION, BASE_SCHEMAS
entry) and buildSubmissionEvent(). PUBLIC + addressable; data in tags: d (id),
k (type), applicant (npub), p (admin), t=bitvid-submission; free-text in content.
- js/submissions/submissionService.js: SUBMISSION_TYPES + labels, parseSubmissionEvent,
buildSubmission, generateSubmissionId. Pure — unit-testable with no relays/signer.
- constants.js: FEATURE_SUBMISSIONS (default OFF until the admin UI + forms ship).
- tests/submission-service.test.mjs: 8 scenarios (build tags/type/default, d-tag
required, parse structure/reject/unknown-type→other, build→parse round-trip of
every type, id generation).
- check-file-size.mjs: bump nostrEventSchemas grandfather 3250→3541 (adding a new
event type to the schema registry is the prescribed pattern).
- docs/nostr-event-schemas.md: catalogue row.
Schema validation ✨, lint, build all pass; tests 8/8.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The create path — makes playlists actually usable end-to-end.
- js/ui/playlistPicker.js: an "Add to playlist" dialog (modeled on confirmDialog)
listing the signed-in user's playlists as checkboxes (checked when the video is
already in them) + a "Create new playlist" input. On Save it diffs the
checkboxes and publishes only changed playlists (add/remove the video) plus any
new one, via the facade (active signer). Reads the current pubkey from
nostrClient, so callers pass only the video coordinate.
- videoMenuRenderers.js: "Add to playlist" item in the video ⋯ menu, gated by a
new canManagePlaylists param.
- moreMenuController.js: computes canManagePlaylists (FEATURE_PLAYLISTS && logged
in), threads it into the panel context, and adds the add-to-playlist handler
(resolves the video → coordinate via buildVideoAddressPointer, closes the
popover, opens the picker; success/error routed to the app's toasts).
- constants.js: FEATURE_PLAYLISTS flipped ON now that the core loop (create /
add / remove / channel section / playlist view) works. Rollback: set false.
Owner controls on the playlist view (rename / drag-reorder / hard-delete) are a
follow-up; removing every video empties a playlist (drops from the listing).
Syntax + lint + build clean; more-menu-controller tests 9/9.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
New route #view=playlist&pubkey=<hex>&id=<playlist id>:
- views/playlist.html: title + meta + the shared #videoList grid.
- js/playlistView.js: initPlaylistView() reads pubkey+id from the hash, fetches
the playlist (facade), resolves its ordered `a` refs against the app's active
videos (buildVideoAddressPointer keys, same 30078 contract), points the back
link at the creator's channel, and renders via app.renderVideoList with
preserveOrder:true so the playlist's order is kept (not re-sorted by date —
#21). Unresolved refs are skipped; the meta line shows "N of M available".
Gated by FEATURE_PLAYLISTS.
- FEED_TYPES.PLAYLIST + viewManager registration (getApp: getApplication).
The channel-section cards now resolve to a working view. node --check + lint +
build clean. End-to-end browser verification comes with the create/edit slice
(needs a way to actually make a playlist).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- constants.js: FEATURE_PLAYLISTS (default OFF) so nothing is user-visible until
the whole playlist UI ships; flip to preview on unstable.
- js/channelPlaylists.js: wireChannelPlaylists(pubkey) fetches the creator's
playlists (facade) and renders a responsive row of cards (thumbnail + video
count + title) linking to `#view=playlist&pubkey=…&id=…`. Hidden when the flag
is off, no playlists, or the container is missing; a nav-token guards against a
stale fetch resolving after a channel switch. DOM built with textContent (no
innerHTML). Kept out of the 5729-line-capped channelProfile.js.
- channelProfile.js: one-line wireChannelPlaylists(currentChannelHex) hook next
to wireChannelZapTotal (no-op while the flag is off).
- views/channel-profile.html: #channelPlaylistsSection above the video grid.
- tailwind.source.css: .playlist-grid / .playlist-card* (token-only; responsive
via @apply like .bv-grid-video).
Lint + build clean. The playlist view the cards link to lands next.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First slice of docs/zap-tally-plan.md (decisions D1-D5 resolved in the doc):
- ZAP_TALLY_KIND = 30081 (addressable, d=payment_hash → one canonical event
per real payment) + NOTE_TYPES.ZAP_TALLY schema + buildZapTallyEvent
(nostrEventSchemas.js). Video + profile pointers.
- FEATURE_ZAP_TALLY flag (constants.js), default ON per D4.
- zapReceiptValidator.js: extractBolt11Fields (payment_hash + description_hash
from the existing bech32 walker), verifyPaymentPreimage
(sha256(preimage)==payment_hash), verifyBitvidZapTally — the security core:
proves the invoice was paid AND bound to this zap request (description-hash
binding blocks reusing a leaked preimage for another video). Amount is
bolt11-derived, never the tag; pointers come from the embedded zap request.
- Schema-doc row for kind 30081.
Tests: tests/zap-tally-verify.test.mjs (8) — preimage check, bech32 field
round-trip, valid/bad-preimage/replay/zero-sats decisions, builder tag shape
(video + profile). Lint + build green.
No publishing or counting wired yet — next slices per the plan checklist.
Co-Authored-By: Claude Fable 5 <[email protected]>
Trending-pattern clone ranked by sats from kind-9735 receipts:
- js/zapTotals.js: per-pointer receipt aggregation. The feed's metric getter
returns the cached total and schedules a batched fetch (#a/#e chunks via
the L1 SubscriptionManager) for unknown/stale pointers; amounts parse via
nip57 bolt11 with the zap request's amount tag as fallback; receipts are
deduped by id so re-fetches never double-count; zero-receipt answers are
cached (2-min TTL).
- sorters.js: shared count-ranked sorter (metric desc, recency tie-break,
author spread, trusted-muted sink); Trending delegates unchanged, Most
Zapped ranks on runtime.getZapTotal.
- Full wiring: feed definition, view + info popover, change-signal re-rank
(order settles as receipts stream in), sidebar link, viewManager registry,
feedCoordinator case/loader, FEED_TYPES.MOST_ZAPPED, and a
FEATURE_MOST_ZAPPED_FEED flag (default on; off hides the tab).
Co-Authored-By: Claude Fable 5 <[email protected]>
Audit found the search pipeline largely real (token parser, local + NIP-50 relay
search, enforced author/tag/date/has/nsfw/kind filters, wired modal) with dead
spots, now fixed:
- sort: offered in the modal but never applied. New pure sortSearchResults —
recent (default), views (shared view-count cache), trending (views weighted by
recency), longest — applied in searchView.
- duration filter only read nip71.duration, so bitvid's OWN kind-30078 videos
(top-level duration) never matched. Both video types now satisfy duration
filters; same shapes cover the planned live (30311) / shorts kinds.
- textScope now enforced in the local title/description match (all|title|description).
- relay: now scopes the video relay-search to that relay (was parsed-but-unused).
- FEATURE_SEARCH_FILTERS removed: dev-only flag consumed by NOTHING while the
filter UI ships and works everywhere (plumbing + setter deleted, no consumers).
Tests (tests/search-filters.test.mjs, 4 — surface previously had zero): parser
tokens; matcher enforces tags/date/has:magnet/duration for BOTH duration shapes;
views/longest/recent sorts (no input mutation); trending ranks fresh-riser above
stale-hit. Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Makes the card liveness-visibility behavior switchable so it can be A/B
feel-tested, and probes ahead of the scroll for a snappier result.
- CARD_LIVENESS_POLICY (config/instance-config.js): "show-pending" (default,
unchanged) | "hide-foreign" (foreign/ingested cards hidden until a source
verifies; native cards stay show-pending) | "hide-all". Owner's own cards are
always visible. Flip live for testing without a rebuild:
window.__BITVID_CARD_LIVENESS_POLICY__ = "hide-foreign" then refresh/scroll.
Implemented via a data-foreign provenance flag on the card +
getCardLivenessPolicy() in cardSourceVisibility.js.
- LIVENESS_PROBE_PREFETCH_MARGIN (default 600px): both liveness probes were
already viewport-gated (IntersectionObserver, on-screen-first, WebTorrent
priority-queued + concurrency-capped); this expands their rootMargin so cards
just below the fold verify before the user reaches them.
- Tests: three policy scenarios in tests/video-card-source-visibility.test.mjs;
also un-broke 3 pre-existing VideoCard tests by adding the missing
IntersectionObserver JSDOM stub.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
New FEED_TYPES.TRENDING tab: the active (recently-added) source ranked by view
count (recency tiebreak, trusted-muted last), gated by FEATURE_TRENDING_FEED
(default on). Builds on the now-trustworthy view counts (#4).
- createTrendingSorter reads counts from runtime.getViewCount, which resolves
each video's pointer via the app's canonical deriveVideoPointerInfo against the
shared viewCounter cache (new getVideoViewCountSnapshot export) — the same
counts the grid cards already load.
- Live re-rank: viewCounter now emits a coalesced onViewCountsChanged signal;
while the Trending view is active it debounce-re-runs the feed (cheap re-rank
over the in-memory active set, no relay re-fetch), so the order settles into
true trending as counts stream in. Cold cache reads as recency, then improves.
- Pipeline extracted to js/feedEngine/trendingFeed.js (feedCoordinator is at its
size cap; bumped the cap for the thin load/refresh delegations).
- Wiring: FEED_TYPES + flag, feedCoordinator cases, app.js wrappers,
applicationBootstrap registration (hides the sidebar link when flag off),
viewManager init + js/trendingView.js, views/trending.html, sidebar link.
Tests: tests/trending-sorter.test.mjs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Addresses "some NIP-71 videos don't play / he has multiple links" + "speed
the liveness check + don't render unplayable".
- Phase 1 — multi-source capture: nip71IngestAdapter collectVideoSources()
exposes a `sources` list (all video imeta urls, in order; audio/image
excluded; deduped) so mirrors are preserved (the adapter previously kept
only one url).
- Phase 2 — multi-source liveness: urlHealthController.probeUrlList() probes
every candidate (primary + sources) via the existing video-element
playability probe — healthy as soon as one source plays, offline only if
ALL fail. So a dead primary host no longer hides a video with a live
mirror. (Card hide policy in cardSourceVisibility was already correct.)
- Phase 3 — speed: URL_PROBE_TIMEOUT_MS 8s -> 4s for a fast verdict so dead
cards hide quickly; WebTorrent swarm probe stays generous.
Remaining (TODO #5): player-side fail-over at play time; hide-until-verified
for foreign ingested videos. Tests + mutation-verified (collectVideoSources
video-only filter, probeUrlList first-ok/offline/timeout). 1271/1272 unit
(known UploadModal flake).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Foundation for inbound NIP-71 ingest. Adds a single admin switch in the
instance config (config/instance-config.js, default true), re-exported
through config.js and surfaced as a runtime-overridable flag in
constants.js — same pattern as FEATURE_NIP71_MIRROR / ALLOW_NSFW_CONTENT.
A deployer flips one constant to disable all inbound ingest if it ever
causes problems (spam, low-quality content, performance). The ingest
adapter + service (gated on this flag) land in follow-up commits; its
behavioral test will assert the service is inert when the flag is off.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Makes the NIP-71 mirror user-facing via the My Videos tab — the natural
manage-my-videos surface — without touching the single-writer publish path.
- js/constants.js: new FEATURE_NIP71_MIRROR flag (default on). Independent of
FEATURE_PUBLISH_NIP71, which is already wired to the LEGACY 21/22 auto-publish in
videoPublisher.js — flipping that would activate the wrong (non-opt-in) model, so
the opt-in mirror gets its own flag and that legacy path stays dormant.
- js/services/nip71MirrorFlags.js: per-video opt-in persistence
(pubkey+videoRootId, off by default) + pure resolveMirrorToggle decision.
- MyVideosController: per-row "Share to apps" / "Shared ✓" toggle (gated by the
flag), wired to nip71MirrorService.publish/remove. Ineligible videos show a
disabled "Share off" with the reason (private / NSFW-not-allowed-here / needs
hosted URL) — the ALLOW_NSFW_CONTENT moderation gate surfaces here too.
tests/nip71-mirror-flags.test.mjs: persistence scoping + toggle decision
(enabled→remove, eligible→publish, ineligible→blocked). Mutation-verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Follow-up to c79120ba (which only captured the file deletion): removes the
sidebar nav link and the unused DEV_DASHBOARD view constant. No dangling refs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Removes `wss://tracker.btorrent.xyz` from `js/constants.js` due to SSL cert error.
- Updates `scripts/agent/debug_frontend.py` to filter out unavoidable external content errors (CORS, 404, invalid certs) to ensure the debug agent focuses on regression in the codebase.
- Adds completion log for the weekly agent run.
Co-authored-by: PR0M3TH3AN <[email protected]>
- `handleVisibility` only scheduled intervals but didn't trigger an immediate refresh when returning to the tab.
- This caused `tests/unit/services/exploreDataService.test.mjs` to fail (assertion `0 !== 1`).
- Added explicit calls to `refreshWatchHistoryTagCounts` and `refreshTagIdf` when `document.hidden` becomes false.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Add "Dev Tools" button to the sidebar footer menu.
- Configure `js/constants.js` with `DEV_DASHBOARD` view type.
- Refactor `views/dev/agent-dashboard.html` to be compatible with `loadView` (moved styles to body, updated links).
- Add cleanup logic to dashboard script to prevent memory leaks when navigating away.
- Fix duplicate import in `js/nostr/reactionEvents.js`.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Introduce `js/constants.js` with semantic time constants (e.g., `ONE_MINUTE_MS`, `STANDARD_TIMEOUT_MS`) and feature-specific TTLs.
- Export `KIND_MUTE_LIST` from `js/nostrEventSchemas.js` to standardize NIP-51 mute list kind usage.
- Refactor `js/state/cache.js`, `js/nostr/cachePolicies.js`, `js/channelProfile.js`, and various services to use centralized constants instead of hardcoded magic numbers.
- Update `js/nostr/relayConstants.js` to derive values from the shared time constants.
- Replace duplicate definitions of timeouts and retry delays across the codebase.
Co-authored-by: PR0M3TH3AN <[email protected]>
Introduced FEED_TYPES constants in js/constants.js and updated js/app/authSessionCoordinator.js, js/hashChangeHandler.js, js/viewManager.js, and js/app/feedCoordinator.js to use them. This improves maintainability and reduces the risk of typos in feed view names.
Co-authored-by: PR0M3TH3AN <[email protected]>
Introduces a race condition between the primary playback source (URL or Torrent) and a configurable timeout. If the primary source fails to reach the 'playing' state within the timeout window, the system automatically falls back to the alternative source.
- Adds `DEFAULT_PLAYBACK_START_TIMEOUT` to instance config (3000ms).
- Updates `PlaybackService` to implement timeout logic using `withTimeout` wrapper.
- Ensures proper cleanup of stalled attempts (resetting video element, destroying torrent client).
- Added `DEFAULT_PLAYBACK_SOURCE` to `config/instance-config.js` to allow choosing between "url" (default) and "torrent" priority.
- Updated `js/constants.js` to derive `URL_FIRST_ENABLED` flag from the configuration.
- Refactored `PlaybackService.execute()` in `js/services/playbackService.js` to robustly support "Torrent First" logic with fallback to URL, while maintaining "URL First" with fallback to Torrent.
- Added unit tests in `tests/services/playbackService_order.test.mjs` to verify playback order and fallback mechanics.
- Remove `wss://open.video.lukewarlow.uk:443/announce` from `DEFAULT_WSS_TRACKERS` in `js/constants.js`.
- This tracker was returning `net::ERR_SSL_PROTOCOL_ERROR`, causing console noise and connection failures.
- Added `scripts/agent/debug_frontend.py` to help diagnose frontend initialization errors.
Verified with `scripts/agent/debug_frontend.py` that the error is resolved.
Ran `npm run format`, `npm run lint`, and `npm run test:unit`.
- Increased tracker timeout to 6000ms and concurrency to 6 in `js/trackerConfig.js`
- Increased trackers per magnet check to 10 to ensure better peer discovery
- Added new reliable public WebSocket trackers to `DEFAULT_WSS_TRACKERS` in `js/constants.js`
- Verified configuration changes with `tests/verify_tracker_config.mjs`
- Refactored `js/userBlocks.js` to merge Kind 10000 and Kind 30002 events on load, preventing overwritten mutes.
- Updated `js/constants.js` to set `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD` to 4, enabling soft-hide (blur) behavior for lower mute counts.
Removed the `ACCEPT_LEGACY_V1` flag and associated logic that supported parsing magnet links from unstructured text notes or tags, as well as support for video events with version < 2.
This simplifies `js/videoEventUtils.js` and `js/nostr/nip71.js`, and removes legacy regex scanning.
Deleted `tests/legacy-infohash.test.mjs` which tested this deprecated functionality.
- Introduce FEATURE_IMPROVED_COMMENT_FETCHING flag (default: true in unstable)
- Enhance createVideoCommentFilters to include comprehensive union of legacy (#e/#a) and uppercase (#E/#A/#I/#K/#P) tags for NIP-22 compliance, reducing misses in mixed-relay propagation
- Add localStorage caching (5min TTL) in CommentThreadService.fetchThread for fast loads post-reload; includes getCachedComments, cacheComments, and clearCommentCache
- Force full-history re-fetch in VideoModalCommentController.load with retry logic if low count, plus background refresh after 1s
- Bootstrap cache warm-up for recent videos on bitvidApp init (tracks via localStorage recentVideos list)
Fixes: Comments posted not reloading in video modal (root cause: incomplete filters + no persistence)
Closes: #N/A (internal fix)
QA: Verified post → reload → modal open shows comments via cache/query; no regressions in threading/UI
Rollout: Gated by flag; rollback by setting false + clear localStorage (^comments: keys)