After the note-side list renders, the My Videos tab now reconciles against the
bucket: lists the user's objects (unlock-gated), then marks any active note whose
hosted file is gone as a 🟠 "Missing file", and lists bucket objects no current
video references in an "Orphaned storage objects" section — including files left
behind by videos deleted on Nostr (the headline cleanup case). Each orphan has a
"Delete file" button (with a confirm) that calls deleteStorageKeys and refreshes.
Reconciliation runs after the list is shown so the UI stays responsive, and is a
no-op (orphan section hidden) when storage is locked/unconfigured. Rows carry a
data-video-id + a data-role pill so missing status can be applied in place.
Build + full lint green (DOM via createElement/textContent, no innerHTML).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Pure diff of the user's notes against their bucket contents: reconcileStorage
returns MISSING (an active hosted note whose primary video file is absent) and
ORPHAN keys (bucket objects no live note references — including a deleted video's
leftover file, the "deleted on Nostr but still in the bucket" case). Safety
guarantees: a file an active note still uses, plus its sibling .torrent and
thumbnail, is never reported as an orphan; a missing sibling does not mark a row
missing (only a missing primary video file does); external-URL videos can't be
"missing" but unrelated bucket files are still orphans.
Tests + mutation-verified: orphan = present minus referenced (not all present),
and the missing-primary check, are both independently guarded.
Build + full lint green. Next: r2Service.listVideoStorageObjects + wiring.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Each My Videos row now shows the video's thumbnail (lazy, with a placeholder box
that survives a broken/missing image) and, for active videos, Edit and Delete
buttons that route to the app's existing handleEditVideo / handleFullDeleteVideo
flows (closing the full-screen profile modal first so those modals aren't hidden
behind it). Delete reuses the app flow that already cleans the live URL's storage.
Deleted videos intentionally have no per-row action yet: the tombstone scrubbed
their URL, so reclaiming their orphaned bucket file requires the Phase 2 bucket
listing/reconciliation (and Restore) — added there so it's done correctly.
Build + full lint green (DOM via createElement/textContent, no innerHTML).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds a "My Videos" tab to the profile modal that lists every video the logged-in
user has published — collapsed to one row per video in its current state
(including tombstoned ones) — each with a note-side health pill: red "No playable
source", grey "Deleted on Nostr", green "OK". Shows a summary line
(N videos · M need attention · K deleted) and refreshes from relays on open.
- New MyVideosController (lazy-populated via selectPane, like the other panes):
fetchVideosByAuthors -> collapseUserVideos -> classifyVideoHealth -> render.
Rows built with createElement/textContent (no innerHTML; user titles/URLs are
not interpolated into markup). Status pills reuse the relay-pill token styles.
- Sidebar nav button + pane markup in components/profile-modal.html.
- Wired into profileModalController (cache refs, listeners, selectPane hook);
the existing nav-button loop auto-binds the click.
This is display-only; later steps add the hosted-URL HEAD probe (dead-url), row
actions (edit/delete/revert/storage cleanup), and Phase 2 bucket reconciliation.
Build + full lint green (no new file-size/innerHTML/token violations).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Second pure piece of the "My Videos" tab: collapseUserVideos reduces all of a
user's video events (every edit/revert/deletion the client holds) to one entry
per addressable video, keeping the latest version — which carries the current
state, including deleted:true when the latest is a tombstone (those are gone from
the active feed but still matter here for storage cleanup). Filters to the owner
only, drops invalid notes, sorts newest-first.
Tests + mutation-verified: owner filtering, dedup-to-latest, deleted-latest
preserved, re-publish-after-delete wins, invalid dropped, empty-pubkey returns
nothing (never leak all videos). Flipping latest->oldest fails 3 scenarios.
Build + full lint green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First piece of the "My Videos" management tab: classifyVideoHealth decides the
maintenance status for one of the user's own videos from note-side data plus an
optional URL probe result. Pure/synchronous so it's deterministic and testable;
the controller will do the actual HEAD probe and pass the result in.
Statuses: no-source (no url AND no magnet) -> error; hosted-URL-unreachable ->
warning; deleted (tombstoned) -> info; otherwise ok. Key guardrail: only URLs
under the user's storage publicBaseUrl can be flagged dead — external/CORS-opaque
links are never cried wolf on. Deleted is checked before no-source since
soft-delete scrubs url/magnet.
Tests + mutation-verified: dropping the hosted guard (external false-alarm) and
removing deleted-precedence each fail a scenario.
Build + full lint green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Relay tab improvements so users can see which relays to remove:
- Per-relay status pill: Online (with ping) / Offline / Checking… / Blocked.
- Insecure remote ws:// relays (CSP-blocked) flagged immediately with a hint.
- Offline relays show a "consider removing" hint; their Remove button is
emphasized.
- Summary banner when relays are down (incl. the all-dead case explaining why
videos won't load), pointing at Restore defaults / healthy relays.
Also fixes a pre-existing decomposition bug (commit 7300977a): handleRelayOperation
called this.runRelayOperation, but that method lives on the main
ProfileModalController — so ALL relay ops (add/remove/mode/restore) threw
"this.runRelayOperation is not a function". Route via this.mainController.
Tests: relay-insecure-url (isInsecureRelayUrl), relay-operation-routing (routes to
mainController.runRelayOperation, guards no-pubkey). Both mutation-verified.
Build + lint + full unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The controller→Actions split left a batch of method calls in
ProfileDirectMessageActions pointing at `this` (the Actions instance)
instead of the object that actually defines them. Opening a conversation
threw "this.setFocusedDmConversation is not a function"; the same latent
bug also broke DM send-error messaging, read receipts, settings publish,
and the whole attachment flow (generate/render/reset/upload) — the user
just hadn't exercised them yet.
Route each to its real receiver:
- DM-controller methods → this.controller.*:
handleDmSettingsPublish, describeDirectMessageSendError,
maybePublishReadReceipt, setFocusedDmConversation
- main-controller methods → this.mainController.*:
generateAttachmentId, renderAttachmentQueue, resetAttachmentQueue,
uploadAttachmentQueue, clearProfileMessages
(Builds on the earlier handleDmConversationSelect delegation fix.) A static
sweep confirms every remaining `this.*` call in Actions resolves to an
Actions method or an explicit controller/mainController/helper receiver.
Test: tests/dm-conversation-select.test.mjs (cheat-resistant — drives the
real handleDmConversationSelect with stub receivers; would throw on the old
wrong-receiver code). DM unit + integration suites green; full build OK.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
After switching to a responsive signer (nos2x), two DM-specific bugs surfaced.
1. Clicking a conversation in the list threw
"this.controller.handleDmConversationSelect is not a function"
(ProfileDirectMessageRenderer onSelectConversation). The method exists on
ProfileDirectMessageActions and the controller delegates ~15 sibling Actions
methods — but the handleDmConversationSelect delegation was missing. Add it.
2. Loading a conversation's messages threw "DM decryption helpers are
unavailable" even though the signer decrypts blocks/subscriptions/hashtags
fine. buildDmDecryptContext gated decryptor registration on (a) the dynamic
`capabilities` getter (which probes window.nostr and false-negatives on lazy
injection / nos2x's module shape) and (b) the permission-probe result. The
list services already learned to register by METHOD EXISTENCE and validate at
call time; align the DM path to do the same so a present, working signer
never yields zero decryptors. The permission probe still runs.
Tests: tests/dm-decrypt-context.test.mjs (cheat-resistant — a signer whose
capabilities getter reports false and whose permission probe returns ok:false
must still register nip04+nip44 decryptors). Full unit + dm:unit + dm:integration
suites green. KNOWN_BUGS #0.
Co-Authored-By: Claude Opus 4.8 <[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]>
Extracted DM Relay UI logic and DM Attachment logic into separate binder modules to reduce the size of ProfileModalController.js. Updated file size baseline.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Added delegate methods for `refreshAdminPaneState`, `refreshModerationSettingsUi`, `updateModerationTrustStats`, `runAdminMutation`, `sendAdminListNotification`, `describeAdminError`, `describeNotificationError`, `notifyAdminError` to `ProfileModalController.js`.
- Added aliases for properties like `moderatorSection` in `ProfileModalController.js` to satisfy tests.
- Removed duplicated method implementations from `ProfileModalController.js`.
- Verified with unit tests (`tests/profile-modal-controller.test.mjs`) and linting.
- Reduced `ProfileModalController.js` size by ~2000 lines.
Co-authored-by: PR0M3TH3AN <[email protected]>
Extracts all hashtag preference related logic into a new controller
`js/ui/profileModal/ProfileHashtagController.js` to reduce the size of `js/ui/profileModalController.js` and improve maintainability.
- Moves `normalizeHashtagTag`, `formatHashtagTag`, `getResolvedHashtagPreferences`, `setHashtagStatus`, `refreshHashtagBackgroundStatus`, `populateHashtagPreferences`, `renderHashtagList`, `sanitizeHashtagList`, `createHashtagListItem`, `persistHashtagPreferences`, `handleAddHashtagPreference`, `handleRemoveHashtagPreference`, `handleHashtagPreferencesChange`, `describeHashtagPreferencesError`.
- Updates `ProfileModalController` to delegate to `ProfileHashtagController`.
- Adds delegate getters/methods in `ProfileModalController` to maintain backward compatibility for tests.
- Reduces `js/ui/profileModalController.js` by ~600 lines.
- Updates file size baseline in `scripts/check-file-size.mjs`.
Passes `npm run lint` and `npm run test:unit`.
Co-authored-by: PR0M3TH3AN <[email protected]>
Extracts all hashtag preference related logic into a new controller
`js/ui/profileModal/ProfileHashtagController.js` to reduce the size of `js/ui/profileModalController.js` and improve maintainability.
- Moves `normalizeHashtagTag`, `formatHashtagTag`, `getResolvedHashtagPreferences`, `setHashtagStatus`, `refreshHashtagBackgroundStatus`, `populateHashtagPreferences`, `renderHashtagList`, `sanitizeHashtagList`, `createHashtagListItem`, `persistHashtagPreferences`, `handleAddHashtagPreference`, `handleRemoveHashtagPreference`, `handleHashtagPreferencesChange`, `describeHashtagPreferencesError`.
- Updates `ProfileModalController` to delegate to `ProfileHashtagController`.
- Reduces `js/ui/profileModalController.js` by ~600 lines.
- Updates file size baseline in `scripts/check-file-size.mjs`.
Passes `npm run lint` and `npm run test:unit`.
Co-authored-by: PR0M3TH3AN <[email protected]>
Extracted ProfileDirectMessageController (split into Controller, Actions, Renderer, Helper) and ProfileRelayController from ProfileModalController to reduce file size and improve maintainability.
ProfileModalController.js size reduced from ~12000 lines to ~8750 lines.
Updated unit tests to reflect the new structure.
Co-authored-by: PR0M3TH3AN <[email protected]>