Commit Graph
377 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Fable 5 e32298cc89 uploads: locked-storage file picks unlock inline instead of dead-ending (#56)
All three paths (upload video pick, upload thumbnail pick, edit "Replace
file") now route through the shared signer gate — silent kept-unlocked
restore, else ONE passphrase prompt — then unlock storage and continue the
same pick. Cancel/bad-passphrase abort quietly; a refused thumbnail pick
restores the picker. Unlock glue extracted to uploadModalStorageUnlock.js
(file-size budget); ensureSigner threaded via initUploadModal/initEditModal.

Tests: tests/upload-edit-storage-unlock-gate.test.mjs (12).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 21:01:20 -04:00
thePR0M3TH3ANandClaude Opus 4.8 9afbc0cbbb [search] Decode npub authors to hex — the Author filter matched nothing (npubs)
The Author field (and the author: token) passed values through as-typed, but the
matcher compares against video.pubkey — HEX. So entering an npub (the only format
users actually have) silently returned zero results; only raw hex worked.

New normalizeAuthorValues (searchFilters.js, injectable decoder for tests):
npub1* decodes via safeDecodeNpub to hex, hex passes through lowercased, and
undecodable values are dropped — surfaced as parse errors on the author: token
path. Applied in BOTH the parser and the filter modal's applyFilters, so the
Author panel field and the token syntax now genuinely filter by creator.

Tests: +2 in tests/search-filters.test.mjs (decode/passthrough/drop with an
injected decoder; parser hex passthrough + garbage error). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 23:12:20 -04:00
thePR0M3TH3ANandClaude Opus 4.8 5466d97544 [ui] Remove NIP-71 authoring sections from upload/edit modals (#58)
bitvid authors kind-30078 and MIRRORS to NIP-71; it never authors NIP-71 directly.
Verified before removing: the My Videos mirror (buildNip71MirrorEvent) derives
everything from the bitvid note itself (duration/dims are auto-captured at upload,
contentWarning comes from the NSFW toggle, imeta is built from the video), and the
only consumer of the form's NIP-71 metadata (publishNip71Video) is gated behind
FEATURE_PUBLISH_NIP71: false — the fields fed a dead path and confused uploaders.

Removed from the upload modal: "NIP-71 Technical" (content warning, duration,
summary + Customize toggle) and the IMETA variant editor. Removed from the edit
modal: kind (21/22) selector, published_at, alt, duration, content warning,
summary, IMETA, text tracks, segments, participants, references. KEPT in both:
hashtags (they feed hashtag feeds, the suggestion chips, and the mirror's t tags)
and the web seeds / torrent (xs) inputs (bitvid-note fields, not NIP-71). All the
JS plumbing is missing-DOM tolerant, so no wiring changes were needed to collect.

Compatibility: existing videos' NIP-71 extras survive a save. EditModal keeps the
video's full stored metadata (storedNip71Metadata) and merges the edited hashtags
into it on submit instead of replacing it with the reduced form's empty output
(t mirror key kept in sync).

Also fixes a latent pre-existing bug the reduction exposed: nip71Edited compared
the submit-side copy AFTER publishedAt normalization (string→timestamp,
delete-empty) against an UN-normalized original, so any video with publishedAt
made every save look "edited". Both sides now share normalizeCollectedNip71.

Tests: +2 in edit-modal-submit-state (stored extras survive a hashtag edit;
untouched save keeps nip71Edited false). Modal/payload/mirror suites green
(39 + 7). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 15:18:10 -04:00
thePR0M3TH3ANandClaude Opus 4.8 6e2894e3d8 [video-modal] Fix dead like/dislike buttons — dispatch video:reaction (#54)
The real #54 bug (the signer gate was correct but unreachable): the reactions
controller forwards the raw click event to VideoModal.handleReactionClick, which
called this.reactionsController.handleReaction(event) — a method that does NOT
exist on the video-modal reactions controller (it only binds buttons + does
optimistic UI), passing the DOM event as the "reaction". So clicking like/dislike
threw / silently did nothing and never reached the publish path.

Fix: handleReactionClick now derives +/- from the clicked button
(#modalLikeBtn / #modalDislikeBtn, or a bare "+"/"-" for programmatic callers) and
dispatches "video:reaction" — the event ModalManager already routes to
app.handleVideoReaction -> reactionController.handleReaction (signs + publishes the
kind-7, and runs the ensureEncryptionCapableSigner gate from the earlier #54 pass).

Note: #53 (channel avatar/name link) is already fixed in a20388c5
(handleCreatorNavigation now dispatches "creator:navigate"); if it still looks dead
it's a stale build — rebuild dist / redeploy the branch under test.

Tests: tests/video-modal-controllers.test.mjs (+2). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 09:40:46 -04:00
thePR0M3TH3ANandClaude Opus 4.8 757fca1059 [edit] Host the .torrent (xs=) for edited external URLs too — close the gap
The edit-modal external-URL webseed was ws=-only because the Edit modal had no
storage credentials wired in. Reuse mediaUploader.resolveActiveConnection() (the
same resolver the Edit modal's file-replace uploads use) to get the active
provider + credentials when storage is unlocked, and pass them to
deriveTorrentForExternalUrl so it hosts the tiny .torrent and emits ws=+xs= — full
P2P bootstrap, matching the upload modal. Storage locked / no connection → ws=-only
(unchanged, best-effort).

Edit-modal submit-state + upload-field + external-url-torrent suites green.
Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 09:17:32 -04:00
thePR0M3TH3ANandClaude Opus 4.8 11c7884366 [edit] Derive a webseed torrent when editing a video's URL to an external link
Mirror the upload-modal external-URL webseed for the Edit modal: when the user
changes a video's URL to an external link with no magnet, auto-derive the webseed
torrent (fetch + hash the remote file → magnet ws=url) so an edited external URL
keeps the P2P benefit. Best-effort + size-capped; on CORS / too-large / any error
it stays URL-only. Runs before the existing magnet normalization so the derived
magnet's ws/xs are extracted the same way a pasted magnet's are.

submit() is now async (it was fire-and-forget from the form handler; the
pendingSubmit guard already prevents re-entry, so it just stays "pending" during
the derivation).

Known gap: hosting the tiny .torrent (xs=) on the edit path needs storage-
credential plumbing the Edit modal doesn't currently have — so the edit path is
ws=-only for now (upload path hosts xs= when storage is unlocked). Tracked as a
follow-up.

Edit-modal submit-state + upload-field tests green. Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 09:12:14 -04:00
thePR0M3TH3ANandClaude Opus 4.8 cf6a830183 [upload] Derive a webseed torrent for external-URL posts (P2P for external links)
Posting an external URL used to skip the torrent/webseed benefit entirely. Now the
upload modal's external tab auto-derives a WebTorrent webseed from the URL at
publish time: fetch the remote file (CORS-permitting), stream it under a hard size
cap, compute the infoHash, and publish a magnet with the URL as the webseed (ws=).
Best-effort — CORS failure / over the cap / any error silently degrades to
URL-only. Only runs when the user didn't paste their own magnet.

- js/utils/externalUrlTorrent.js: fetchBlobWithCap (streams with a 500 MB cap,
  aborts past it, short-circuits on content-length), buildWebseedMagnet, and
  deriveExternalUrlTorrent (reuses createTorrentMetadata). Pure + injectable → unit
  tested without network/WebTorrent.
- mediaUploader.deriveTorrentForExternalUrl: derives the ws= magnet and, when
  storage is unlocked, also hosts the tiny .torrent so the webseed can actually
  bootstrap P2P (xs=) — best-effort, falls back to ws=-only.
- UploadModal.handleExternalFlow calls it; deriveExternalWebseed only hosts the
  .torrent when storage is unlocked, shows progress on the submit button, and
  never throws.

Tests: tests/external-url-torrent.test.mjs (9 — magnet assembly, name parsing,
streamed size-cap enforcement + short-circuit, fetch-failed, derive-with-injected-
hash, failure propagation). Lint + build green.

Next: the same for editing a URL in the Edit modal.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 09:08:32 -04:00
thePR0M3TH3ANandClaude Opus 4.8 a20388c5d6 [video-modal] Fix dead channel/creator link — event-name mismatch (#53)
Clicking the creator avatar/name in the video player modal did nothing. VideoModal
dispatched "navigate:profile", but ModalManager only listens for "creator:navigate"
(→ app.openCreatorChannel), so the event went into the void — same class as the old
action:embed bug noted just above the handler.

Fix:
- VideoModal.handleCreatorNavigation dispatches "creator:navigate" (the listened name)
  with { pubkey }.
- ModalManager's handler forwards event.detail.pubkey to app.openCreatorChannel.
- routerCoordinator.openCreatorChannel(pubkey) accepts an optional pubkey (prefers it,
  falls back to the active video's creator), so it navigates to the modal's own
  creator rather than whatever currentVideo happens to be.

Tests: tests/video-modal-controllers.test.mjs (+2 — dispatches creator:navigate with
the pubkey and never the dead navigate:profile; no-op without a pubkey). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 08:51:10 -04:00
thePR0M3TH3ANandClaude Opus 4.8 3df398d7ad [upload] Suggest one-tap hashtag chips from the user's past videos (#45)
The upload modal's Hashtags section now shows chips of the user's most-used past
hashtags ("Reuse a tag from your videos:"). Clicking one adds it to the NIP-71 't'
repeater; already-added chips dim + disable; the row is hidden for users with no
past tags.

- js/utils/hashtagSuggestions.js: pure rankHashtagsByFrequency(videos) — count =
  number of the user's videos using each tag, normalized (case + '#') and deduped
  per video, ranked by count then alphabetically. Reads video.nip71.hashtags, falls
  back to raw event 't' tags minus bitvid's fixed t=video marker.
- app.getUserHashtagSuggestions() sources the user's own videos via
  nostrService.getActiveVideosByAuthors([pubkey]) and ranks them (top 12), injected
  into the modal as getHashtagSuggestions.
- UploadModal renders the chips on open and wires clicks through the existing
  Nip71FormManager (addRepeaterEntry + setFieldValue, dedup via collectRepeaterValues).

Tests: tests/hashtag-suggestions.test.mjs (5 — per-video counting, normalization,
marker exclusion, tie-break + limit, empty input). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 20:59:56 -04:00
thePR0M3TH3ANandClaude Opus 4.8 6bdf28fb30 [upload] Auto-unlock storage in the upload modal for a kept-unlocked session (#51)
The upload modal only READ storage's unlock state (isUnlocked) on load/open/refresh
— it never auto-unlocked the way the profile Storage pane does. So after a refresh
with "keep unlocked" ticked, the upload modal still opened with storage Locked even
though the signer had been restored.

Add UploadModal.maybeAutoUnlockStorage(pubkey), called on load(), open(), and
refreshState(): if storage is locked but a decrypt-capable signer is already present
(e.g. a kept-unlocked nsec restored on refresh), unlock silently. Guarded two ways:
  - only when the account ALREADY exists (new storageService.hasStoredAccount) so it
    never auto-creates storage for magnet/URL-only uploaders, and
  - only when the signer is already available, so it never triggers a fresh
    permission prompt on modal open.

Tests: storageService.hasStoredAccount (false before setup, true after unlock,
never creates an account, survives lock). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 20:20:57 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ad83c9f8db [ui] route all native alert()/confirm() through the site notification system
Every browser alert()/confirm() now uses the app's UI instead of native dialogs
(no prompt() existed). Two new pieces:

- js/ui/confirmDialog.js — showConfirm(message, opts) -> Promise<boolean>, a styled
  `bv-modal modal-always-on-top` dialog (Confirm/Cancel/backdrop/Escape; danger variant
  focuses Cancel). Importable anywhere, so future confirmations use it too. It stacks
  above other modals and is typable thanks to the recent stacked-modal focus fix.
- js/ui/appNotify.js — notifyError/notifyStatus/notifySuccess routing to the app's
  NotificationController, for modules without a local showError; re-exports showConfirm.

Migrated: UploadModal (10 alert -> this.showError, 1 confirm), subscriptionHistory
(3 alert -> notify*, 1 confirm), MyVideosController (2), ProfileStorage/Wallet/Relay
controllers, profileModalController (2), settingsRestorePrompt (default confirm now
showConfirm), nostr/client deleteAllVersions. confirmSyncOverwrite helpers made async
(syncConflict already awaits confirmOverwrite).

Tests: tests/confirm-dialog.test.mjs (6 — confirm/cancel/backdrop/Escape resolution,
DOM cleanup, danger focus, no-DOM fallback). Build + lint clean; affected suites green.
(Note: build/lint don't parse JS — caught a multi-line-import split + a wrong import
path via module-import checks.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 16:49:42 -04:00
thePR0M3TH3ANandClaude Opus 4.8 5afd602d52 [ui] upload: route by the credentials' own provider (fix "S3 endpoint is required")
With both Backblaze B2 and Cloudflare R2 configured, an upload could fail with "S3
endpoint is required." when the upload modal's tracked provider drifted out of sync
with the active credentials (user: storage selector on Backblaze but the modal set for
Cloudflare). A Cloudflare R2 connection stores an accountId and derives its endpoint, so
routing it through the S3 service — which requires an explicit endpoint — threw.

Fix: MediaUploader.uploadVideo/uploadThumbnail now choose the service + provider from the
CREDENTIALS' own provider (credentials.provider || credentials.meta.provider || provider),
so a stale modal provider can no longer send an R2 connection through the S3 path (or
vice-versa). The R2 service derives the endpoint from accountId as before.

Test: tests/media-uploader-provider-routing.test.mjs (4 — R2 creds route to r2Service
even with a stale Backblaze arg; B2 creds route to s3Service; meta.provider + passed-arg
fallbacks). Build + lint clean; upload/storage suites green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 15:35:18 -04:00
thePR0M3TH3ANandClaude Opus 4.8 f00d2b5f49 [ui] upload modal: prompt for the nsec passphrase instead of dead-ending after reload
With a persisted ("remember this key") nsec login, uploading a video after a page
reload reported "storage is locked" and then "No signer available to unlock storage."
The reload restores the logged-in pubkey but NOT the in-memory signer (the key is
passphrase-encrypted), and UploadModal.handleUnlock dead-ended on the alert — it never
asked for the passphrase.

Fix (mirrors the storage pane's #36 behavior): handleUnlock now calls
promptStoredNsecUnlock(pubkey) when there's no usable signer. If the active account has
a locked saved nsec key (getStoredSessionActorMetadata), it shows an actionable message
and opens the login modal's existing unlock-saved-key (passphrase) flow via a new
onRequestUnlock hook (wired in initUploadModal -> app.loginModalController.openModal),
instead of the dead-end alert. After the user re-enters their passphrase the signer is
active and the upload proceeds.

Tests: tests/upload-modal-nsec-unlock.test.mjs (5 — detects locked nsec for the active
account + opens unlock; ignores missing/non-nsec/other-account; falls through with no
hook). Build + lint clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 15:24:29 -04:00
thePR0M3TH3ANandClaude Opus 4.8 3e57ab2ea9 [moderation] #25: per-video admin block list (hide a single video, not the author)
Admin moderation was author/community-level only. Admins (any editor — same gate as
the author blacklist) can now hide a SINGLE offending video.

- Record: a published kind-30000 NIP-51 list under d-tag bitvid:admin:event-blacklist,
  carrying blocked event ids as `e` tags (NOTE_TYPES.ADMIN_EVENT_BLACKLIST schema).
  Loaded/persisted through the existing admin-list machinery (adminListStore) and cached
  like whitelist/blacklist.
- accessControl: eventBlacklist Set + getEventBlacklist/isEventBlacklisted/
  addToEventBlacklist/removeFromEventBlacklist (editor-gated; normalizes hex or
  nevent/note) + onEventBlacklistChange.
- Render filter already existed (app.blacklistedEventIds, honored by every feed/grid/
  search/playback path) but was fed only by the static ADMIN_INITIAL_EVENT_BLACKLIST
  config. app._rebuildBlacklistedEventIds() now merges that static base with the dynamic
  admin list, and an onEventBlacklistChange listener rebuilds + refreshes grids on change.
- UI: a "Block this video" item in the per-video ⋯ menu (editor-only), handled by
  handleBlacklistEventAction (js/ui/moreMenu/blacklistEventAction.js).

Helpers extracted to js/adminEventBlacklistHelpers.js to keep adminListStore /
accessControl / moreMenuController under their size caps. Tests:
tests/admin-event-blacklist.test.mjs (9). Build + lint clean; admin/access/moderation
suites 56/56.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 15:19:34 -04:00
thePR0M3TH3ANandClaude Opus 4.8 e79595b902 [storage] #38: add Backblaze B2 as a first-class storage provider
B2 doesn't map onto the generic-S3 "paste an endpoint" model: its S3-compatible
endpoint is region-scoped (s3.<region>.backblazeb2.com) and a public bucket is
addressed virtual-hosted-style, so the public download URL is
https://<bucket>.s3.<region>.backblazeb2.com/<key> — not derivable from a pasted
generic endpoint. So B2 silently failed under "Custom S3".

- s3-url: deriveB2Endpoint(region) -> https://s3.<region>.backblazeb2.com (empty for
  auto/blank/pasted-host so callers can require a real region); re-exported from
  s3Service.
- storageService: PROVIDERS.B2 + resolveB2Endpoint (explicit endpoint wins, else
  derive) + PROVIDER_TESTS[B2] (derived endpoint, forcePathStyle false).
- Public URL = S3-style virtual-hosted, auto-derived; an explicit Public Access URL
  (custom domain/CDN) overrides it. The save path now forwards the user's public URL
  to prepareS3Connection (also fixes generic-S3 ignoring a custom CDN URL when blank
  -> still derives, so no regression).
- UI: "Backblaze B2" provider option + helper; raw Endpoint hidden for B2 (region
  drives it), region shows a us-west-004 example, force-path-style toggle hidden;
  save/test derive endpoint from region with a clear "enter your region" error.
  UploadModal shows B2-specific labels.
- Tests: tests/storage-b2-provider.test.mjs (8). All s3/storage suites green (50);
  build + lint clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 10:05:53 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ccbe8d9bb2 feat(liveness): config-driven card visibility policy + probe prefetch (#5)
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]>
2026-06-25 09:13:52 -04:00
thePR0M3TH3ANandClaude Opus 4.8 63dcb5f6ed fix(more-menu): make ⋯ "Event Details" robust + non-silent (#31)
The wiring was structurally intact (menu item event-details → handler case →
handleEventDetailsAction → EventDetailsModal.open), so the failure was silent:
- handleEventDetailsAction only opened the modal `if (… && payload.video)` and
  never caught the async open() rejection — any miss = nothing happens.
- EventDetailsModal.open ran the (throwable) renderVersion BEFORE openStaticModal,
  so a render hiccup left the click looking dead.

Hardened:
- handleEventDetailsAction surfaces "No video selected" / "unavailable" instead of
  failing silently, falls back to app.eventDetailsModal, and .catch()es open().
- open() now shows the modal (openStaticModal) FIRST, then renders in a try/catch.
- The ⋯ popover is dismissed before opening the modal.

Couldn't repro by inspection, so this both fixes the likely causes and makes any
remaining failure visible (a toast / console warn naming the exact reason).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 17:53:32 -04:00
thePR0M3TH3ANandClaude Opus 4.8 575befb83c feed: add public popularity (views-over-time) chart in the ⋯ menu (#26)
A "Popularity" item in the video ⋯ menu opens a public views-over-time chart
(view data is public). Builds on the now-trustworthy view counts (#4).

- buildViewCountTimeSeries buckets raw kind-30079 view events by the counter's
  dedupe window (one view per viewer per day) into a cumulative series.
- buildViewCountChartSvg renders a token-colored (currentColor / text-accent)
  SVG area+line chart — no charting library, no raw colors.
- The modal fetches via listVideoViewEventsWithDefaultClient, shows the total,
  and updates LIVE by subscribing to view events (debounced re-render); cleans
  up on close (Esc / backdrop / ✕). Lazy-loaded from the menu handler so it
  only ships when opened.

New size tokens for the modal; reuses .ds-overlay-backdrop for the scrim.

Tests: tests/view-count-chart.test.mjs (cumulative dedupe, junk filtering,
SVG line/area + empty/single-point).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 13:51:41 -04:00
thePR0M3TH3ANandClaude Opus 4.8 084faa3543 video-modal: fix copy-magnet button doing nothing (wrong event name)
Same dead-event bug as the embed button: handleCopyRequest dispatched
"action:copy", but ModalManager listens for "video:copy-magnet"
(app.handleCopyMagnet) and #copyMagnetBtn has no popover fallback — so the
button did nothing. Dispatch "video:copy-magnet". Extended the regression
test to cover it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-23 20:11:38 -04:00
thePR0M3TH3ANandClaude Opus 4.8 a5b284682c video-modal: fix Embed button doing nothing (wrong event name)
handleEmbedRequest dispatched "action:embed", but ModalManager listens for
"video:embed" (which opens EmbedVideoModal) and dispatch() does not remap
names — so the embed button fired an event into the void and the modal
never opened. (Share survives the same mismatch only because shareBtn also
toggles a popover directly.) Dispatch "video:embed". Regression test added.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-23 20:09:27 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c5589a1514 zap: fix NWC connection, dialog UX, and receipt reporting/discovery
- NWC connection was fully broken: parseNwcUri passed a hex secret to
  nostr-tools getPublicKey (needs Uint8Array) and threw. Convert hex->bytes.
- A successful zap was shown as a red error when the kind-9735 receipt
  couldn't be validated. The NWC preimage already proves payment, so a
  paid-but-unconfirmed share is now a success-with-note, not a failure.
- Receipt discovery: query by the reliably-indexed #e/#a/#p tags instead of
  #bolt11 (which most relays don't index), and poll a few times since the
  receipt is published shortly after payment.
- In-flight "Sending..." status no longer mis-toned as a warning; success
  path fully resets the form; an in-flight-open click no longer cancels the
  open (which made the popover need many clicks); fee fallback verified
  no-bypass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-23 14:53:38 -04:00
thePR0M3TH3ANandClaude Opus 4.8 115521928c feat(video): capture & persist dimensions/duration at upload (enables shorts)
Auto-captures a video's width/height/duration at file-select and persists them on
the 30078 — the durable foundation for a future "shorts" vertical feed (filter on
height > width) and for the NIP-71 mirror's short-kind selection.

- js/utils/videoProbe.js: probeVideoMetadata(src) loads metadata into a hidden
  <video> (element injectable for tests); resolves {width,height,duration} or null.
- UploadModal: probes the selected file (objectURL) in the background and threads
  width/height/duration into the publish payload.
- videoPayloadBuilder: persists width/height/duration in content (publish) and
  preserves them across edits; normalizers exported.
- convertEventToVideo: exposes video.width/height/duration → the mirror builder
  already uses them (portrait ⇒ 34236 short + imeta dim).

tests: probe resolution (incl portrait + error/timeout → null); content persist +
parse; portrait⇒34236 / landscape⇒34235 mirror selection; edit preserves dims.
Mutation-verified earlier on the mirror short-selection.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 11:44:02 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c8a63c57c6 feat(edit): thumbnail + video file upload in Edit modal (steps 2-3/3)
Brings the Edit modal to upload parity with the Upload modal: re-upload a
thumbnail or video file instead of only pasting URLs.

- Wire storage services (r2/s3/storageService) + pubkey/npub accessors into
  EditModal via ModalManager/initEditModal; EditModal constructs a shared
  MediaUploader.
- New editModalUpload.js holds all the upload glue (file-picker wiring, progress,
  storage-unlock checks, field write-back) to keep EditModal under its file-size
  budget. EditModal calls initEditModalUpload(this, context) in one line.
- edit-video-modal.html: "Replace video file…" (under the URL field) and
  "Replace thumbnail file…" (under the thumbnail field), each with a hidden file
  input + status line.
- On thumbnail upload: writes the new URL into the thumbnail field. On video
  upload: writes url + magnet (+ ws/xs hints). All written via setFieldUnlocked,
  which marks the field edited so EditModal.collect actually persists it (gated
  behind storage being configured + unlocked).

setFieldUnlocked is unit-tested (mutation-verified): without the unlock, the
re-upload would silently not save. Build + full lint + unit suite green.

NOTE: the upload/storage flow itself can't be unit-tested (browser + R2/S3);
pending manual verification of editing a video's thumbnail/file.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 09:09:07 -04:00
thePR0M3TH3ANandClaude Opus 4.8 7bbf0f08b1 refactor(upload): extract shared MediaUploader (step 1/3 edit-upload parity)
Extract UploadModal's storage+torrent upload core into a reusable
js/ui/components/mediaUploader.js (resolveActiveConnection, uploadThumbnail,
uploadVideo incl. infohash/.torrent/magnet) so the Edit modal can reuse it
without duplicating R2/S3 + torrent logic (and without blowing EditModal's
file-size budget). UploadModal now delegates its thumbnail/video upload core to
MediaUploader, keeping its own progress/state/zombie-guard wrappers — behavior
preserved.

UploadModal: 1319 -> 1191 lines. Build + full lint + unit suite green.

NOTE: the upload flow can't be unit-tested (browser + S3/R2 deps); this is a
behavior-preserving extraction pending manual re-test of a normal upload before
Edit upload is layered on top (steps 2-3).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 08:44:43 -04:00
thePR0M3TH3ANandClaude Opus 4.8 f218ec32fa fix(upload): publish uploaded thumbnail URL (was published empty)
Selecting a thumbnail FILE clears+disables the thumbnail text input, but the
publish metadata read only that (now-empty) input — so file-uploaded thumbnails
published as "thumbnail":"" and the image was missing. Prefer the uploaded
thumbnail's URL (thumbnailUploadState.url), falling back to a pasted URL.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 08:27:41 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c62114557c feat(webseed): manage multiple web seeds in the edit modal (slice 3/3)
Completes end-to-end multi-webseed support: the editor can now see, add, and
remove every web seed a video carries.

- magnetShared: promote normalizeWebSeedList to a shared export (videoNotePayload
  now imports it) and add setMagnetWebSeeds(), which replaces a magnet's COMPLETE
  ws= set with exactly the requested list. ensureWebSeeds is append-only, so
  without set-semantics a webseed a user deleted would linger in the magnet;
  setMagnetWebSeeds strips the existing ws= params first, making removal stick.
- edit-video-modal.html: the single "Web seed (ws=)" input becomes a "Web seeds
  (one per line)" textarea with backup-origin help text.
- EditModal: open() populates the field from extractAllWebSeeds() (every seed,
  not just the first). On save, when the field was manually edited the magnet's
  ws set is rebuilt from the field via setMagnetWebSeeds (true add/remove);
  otherwise existing seeds are preserved untouched. The content `ws` field still
  stores the primary (first) seed while the magnet carries the full set.

Mutation-verified (no-strip and no-add both fail the removal/add tests); build,
relevant lints (inline-styles, tokens, innerHTML), and full unit suite green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 08:52:11 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ce7ef9ef2d feat(webseed): upload form accepts multiple web seeds (slice 2/3)
Let uploaders declare a backup origin at creation time so the swarm can keep
streaming when the primary CDN is overloaded.

- upload-modal.html: the single "Web Seed (ws)" input becomes a "Web seeds
  (one per line)" textarea with help text explaining the backup-origin use
  case and the byte-identical-file requirement.
- UploadModal: in upload mode the R2/CDN url stays the primary webseed and any
  textarea entries follow it (passed as [primaryUrl, textarea]); external mode
  already flowed the field through. All parsing stays in the payload layer.
- videoNotePayload.normalizeWebSeedList now splits every entry (array OR
  string) on newlines/commas, so the upload-modal's [url, "a\nb"] shape and a
  plain multi-line string both expand + dedupe correctly.

Mutation-verified (array entries no longer split fails the upload-shape test);
build + full unit suite green. Lints relevant to the change (inline-styles,
tokens, innerHTML) pass.

Note: npm run lint also reports pre-existing file-size violations in unrelated
files (SignerManager, watchHistory, playbackService, webtorrent, etc.) that
grew in earlier commits — none are touched here. Flagging separately.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 08:36:13 -04:00
thePR0M3TH3ANandClaude Opus 4.8 4e5bfaa6af [playback] harden WebTorrent streaming + add forced-source test deep links
WebTorrent (handleTorrentStream) hardening:
- Resolve only when playback is actually viable (loadeddata/canplay/playing),
  not the instant streamTo() is called. A dead/too-slow swarm is no longer
  reported as success, so the caller's fallback can run.
- Pre-playback torrent errors reject (caller falls back); post-playback errors
  go to an onPlaybackError hook instead of a dead reject().
- Mid-stream stall watchdog surfaces an onStall hook ("waiting for peers").
- Fix a <video> listener leak: stream listeners are tracked and torn down
  before the next stream and on cleanup() (the modal reuses one element).
- Stream the largest decodable file from a multi-file torrent and deselect the
  rest (wider ext set: mp4/m4v/webm/mkv/mov/ogv/ogg) so the swarm isn't spent
  downloading files we never play.
playbackCoordinator wires onStall/onPlaybackError to user-facing status.

Forced-source deep links (test affordance):
- The main app honors ?v=<nevent>&playback=torrent|url (mirrors the embed
  player) to force a playback path; a missing source shows a clear message.
- The video share menu gains "Copy WebTorrent URL" / "Copy CDN URL" items that
  copy the forced-source deep link (disabled with a reason when unavailable).

Tests: webtorrent-handlers (resolve-on-readiness, pre/post-ready errors, stall
fires/clears, largest-file select/deselect, no listener accumulation),
playback-forced-source (param normalization), videoMenuRenderers (copy items +
availability). All mutation-verified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 12:15:27 -04:00
google-labs-jules[bot]andPR0M3TH3AN f7712f51a9 fix: initialize video modal share popover with correct arguments
- Update `js/ui/components/VideoModal.js` to correctly initialize the modal share popover using the `createPopover(trigger, renderFn, options)` signature instead of a single config object.
- Fix the flaky e2e test `tests/e2e/search-and-filter.spec.ts` by increasing the test timeout from 15s to 30s to allow more time for feed items to be retrieved from the network.
- Update `tests/e2e/video-modal-share-bug.spec.ts` to attach an `#uiOverlay` wrapper element for the popover so the DOM doesn't crash on tests.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-03-01 20:49:41 +00:00
google-labs-jules[bot]andPR0M3TH3AN f255bdd9bc chore(torch): execute daily agent run for ci-health-agent
- Resolve scheduler context and preflight exclusions
- Select `ci-health-agent` from roster using round-robin logic
- Acquire distributed lock for execution window
- Perform necessary repository check fixes (linting and tests)
- Publish successful completion lock event
- Write final `task-logs` event record
- Output execution summary

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-28 19:48:57 +00:00
thePR0M3TH3AN 34b0b03ad0 Merge branch 'unstable' into agents/daily/torch-garbage-collection-agent-2026-02-26-15639715828522871125 2026-02-26 21:42:21 -05:00
thePR0M3TH3AN 8d6e32be27 Merge branch 'unstable' into agents/daily/style-agent-1740615175-12694853261144843326 2026-02-26 21:40:23 -05:00
thePR0M3TH3AN 9448624753 Merge branch 'unstable' into agents/daily/todo-triage-agent-13829754158937187173 2026-02-26 21:38:12 -05:00
google-labs-jules[bot]andPR0M3TH3AN 5c2ba17459 chore: garbage collection and maintenance fixes
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-27 02:32:53 +00:00
google-labs-jules[bot]andPR0M3TH3AN a649c341e0 I have updated the nostr-tools usage and resolved the UI bugs. Here are the details of the changes:
- Updated `js/nostr/adapters/nsecAdapter.js` to ensure private keys are `Uint8Array` bytes before passing to `getPublicKey`, fixing compatibility with `nostr-tools` v2.
- Updated `tests/nostr/nip46Client.test.js` to use byte arrays for keys in crypto operations.
- Implemented missing `setupModalMorePopover`, `setupModalSharePopover`, and `refreshModalMoreMenuPanel` in `VideoModal.js`.
- Fixed a bug in `VideoModal.js` where `setCommentComposerState` was overriding `CommentsController` send button logic; it now delegates to the controller if available.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-27 02:14:29 +00:00
google-labs-jules[bot]andPR0M3TH3AN 089556acd1 chore(todo-triage): daily scan and minor fixes
- Ran `todo-triage-agent` scan (0 actionable items found).
- Fixed inline style lint error in `tests/ui/components/DeleteModal.test.mjs`.
- Fixed duplicate import syntax error in `js/ui/components/RevertModal.js`.
- Initialized `modalNavScrollHandler` in `VideoModal` constructor to improve test stability.
- Updated scheduler memory.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-27 02:14:06 +00:00
google-labs-jules[bot]andPR0M3TH3AN bf076578e9 fix(ci): resolve nostr-tools v2 compatibility and VideoModal regressions
- Fix `nostr-tools` v2 compatibility:
  - Update `js/nostr/adapters/nsecAdapter.js` to convert hex private keys to `Uint8Array` before passing to `getPublicKey`.
  - Update `js/nostr/nip46Client.js` to ensure keys are bytes for `getPublicKey`.
  - Update `tests/nostr/nip46Client.test.js` and `tests/nostr-private-key-signer.test.mjs` to use `Uint8Array` for keys.
- Fix `VideoModal` regressions:
  - Add missing delegation methods (`setCommentsVisibility`, `clearComments`, `resetCommentComposer`, `setCommentStatus`, `setCommentComposerState`, `appendComment`, `renderComments`, `setCommentSectionCallbacks`, `hideCommentsDisabledMessage`, `showCommentsDisabledMessage`, `updateCommentCharCount`, `updateCommentSubmitState`) to `js/ui/components/VideoModal.js`.
  - Expose getters for `commentsList`, `commentsComposer`, etc., in `VideoModal.js` to support tests.
  - Implement `setCallbacks` in `js/ui/components/video-modal/commentsController.js` and ensure robust initialization when DOM elements are missing.
  - Fix `open()` and `updateSourceAvailability()` in `VideoModal.js` to correctly re-enable Share, Embed, and Copy Magnet buttons.
- Fix `DeleteModal` tests to comply with linting rules (avoid direct `.style` assignment).

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-27 02:08:58 +00:00
google-labs-jules[bot]andPR0M3TH3AN 7427961a69 fix(ci): resolve various test failures and nostr-tools v2 compatibility
- Fix duplicate imports/declarations in `js/ui/components/RevertModal.js`
- Update `js/nostr/nip46Client.js` to pass `Uint8Array` keys to `getConversationKey` for compatibility with `nostr-tools` v2
- Fix NIP-46 compliance tests by using correct key types and improved mocks
- Implement missing methods in `js/ui/components/VideoModal.js` (`handleCopyRequest`, `setCommentsVisibility`, etc.) to fix runtime crashes in tests
- Ensure `VideoModal` stores comment composer state locally to support external controller updates in tests
- Update DM integration tests to use `Uint8Array` keys for `nostr-tools` helpers
- Fix linting issue in `tests/ui/components/DeleteModal.test.mjs` using `Object.defineProperty`

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-27 01:52:00 +00:00
google-labs-jules[bot]andPR0M3TH3AN 6a7dbf8c7d fix(ui): restore missing methods in VideoModal.js to prevent initialization crash
- Restore missing handlers (`handleCopyRequest`, `handleShareRequest`, `handleEmbedRequest`, `handleCreatorNavigation`, `handleModalMoreButtonClick`, `handleReactionClick`) and setup methods (`setupModalMorePopover`, `setupModalSharePopover`, `bindVideoEvents`, `detachVideoEvents`, `bindActionButtons`, `updateSourceAvailability`, `updateSourceToggleState`, `attachAmbientGlow`, `teardownAmbientGlow`, `refreshModalMoreMenuPanel`).
- Fixes `TypeError: Cannot read properties of undefined (reading 'bind')` in `VideoModal` constructor which caused E2E test timeouts and unit test failures.
- Implement methods using standard patterns and available imports (`popoverEngine`, `videoMenuRenderers`, `ambientBackground`).

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-27 01:31:28 +00:00
google-labs-jules[bot]andPR0M3TH3AN 5aac1b2d21 fix(ci): address multiple test and build failures
- Fixed duplicate import and variable declaration in `js/ui/components/RevertModal.js` which caused syntax errors.
- Fixed missing method implementations in `js/ui/components/VideoModal.js` which caused `bind` errors during initialization.
- Fixed NIP-46 compliance tests by updating mocks to match `nostr-tools` v2 structure.
- Updated `js/nostr/nip46Client.js` to ensure keys are passed as `Uint8Array` to `nostr-tools` helpers, maintaining compatibility with legacy hex strings.
- Updated `tests/nostr/dm-direct-message-flow.test.mjs` to use `Uint8Array` keys, fixing "expected Uint8Array" errors.
- Maintained the previous lint fix for `tests/ui/components/DeleteModal.test.mjs`.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-27 01:15:53 +00:00
google-labs-jules[bot]andPR0M3TH3AN 51128c1d33 fix(ci): address linting errors and nostr-tools v2 compatibility
- Fix `SyntaxError` in `js/ui/components/RevertModal.js` by removing duplicate variable declaration.
- Update `js/nostr/nip46Client.js` to support hex string private keys with `nostr-tools` v2 `nip44` operations by converting to `Uint8Array`.
- Update `tests/nostr/dm-direct-message-flow.test.mjs` and `tests/compliance/nip04_44_compliance.test.mjs` to pass `Uint8Array` keys where required.
- Fix linting error in `tests/ui/components/DeleteModal.test.mjs` by avoiding direct `.style` assignment in mock.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-27 00:32:11 +00:00
thePR0M3TH3AN de80e58b5d Merge pull request #2725 from PR0M3TH3AN/refactor-delete-modal-dtag-919037621417068012
Refactor DeleteModal to use shared getDTagValueFromTags utility
2026-02-26 13:04:19 -05:00
thePR0M3TH3AN 0e5e56b146 Merge branch 'unstable' into refactor-revert-modal-dtag-18208308647871133769 2026-02-26 13:04:04 -05:00
thePR0M3TH3AN c28d1e2c2b Merge pull request #2734 from PR0M3TH3AN/refactor/revert-modal-rendering-15253432646628070109
Refactor RevertModal rendering logic
2026-02-26 13:00:38 -05:00
google-labs-jules[bot]andPR0M3TH3AN feb7ad765a Refactor VideoModal to delegate comments to CommentsController
Move comment-related logic and state from VideoModal.js to CommentsController.js.
Split CommentsController into CommentsController (orchestration), CommentsListRenderer (DOM management), and CommentNodeFactory (element creation) to maintain modularity and adhere to file size limits.
Update VideoModal to initialize and destroy CommentsController correctly.
Fix minor lint issues.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-26 17:53:34 +00:00
google-labs-jules[bot]andPR0M3TH3AN 2703382ab4 Refactor RevertModal rendering logic into separate module
Extracted excessive DOM rendering logic from `RevertModal.js` into `revertModalRenderers.js`.
This improves code readability and maintainability by separating the view construction from the modal logic.
Added unit tests for the new renderers using a lightweight DOM mock.
Replaced local d-tag extraction with shared utility `getDTagValueFromTags`.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-26 17:50:10 +00:00
google-labs-jules[bot]andPR0M3TH3AN 4bdcfbdb2e Refactor: Replace duplicated extractDTagValue in DeleteModal
Replaced the local `extractDTagValue` function in `js/ui/components/DeleteModal.js` with the shared `getDTagValueFromTags` utility from `js/nostr/nip71.js` (exported via `js/nostr/index.js`).

Added a new unit test `tests/ui/components/DeleteModal.test.mjs` to verify the d-tag extraction logic and ensure the refactor is correct and safe. This test uses `jsdom` to mock the necessary DOM environment.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-26 17:39:07 +00:00
google-labs-jules[bot]andPR0M3TH3AN 931f57eebe Refactor RevertModal to use shared d-tag extraction utility
- Removed duplicated `extractDTagValue` method from `js/ui/components/RevertModal.js`
- Imported and used `getDTagValueFromTags` from `js/nostr/nip71.js`
- Added unit tests for `getDTagValueFromTags` in `tests/nostr/nip71-d-tag.test.mjs`

This reduces code duplication and ensures consistent d-tag extraction logic.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-26 17:37:26 +00:00
google-labs-jules[bot]andPR0M3TH3AN eb56dbb6a2 fix(ui): resolve race condition in UploadModal by initializing controller early
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-22 23:22:46 +00:00
google-labs-jules[bot]andPR0M3TH3AN c1d7cc768e fix(ui): resolve race condition in UploadModal opening
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-22 22:31:35 +00:00