After watching/opening a video, zapping showed "Log in and connect a wallet"
even though logged in. Recording a view creates an anonymous session actor with
source "session" (SignerManager.ensureSessionActor). isUserLoggedIn() treated any
declaredSource on a different-pubkey session actor as a managed alternate
identity and returned false — so the view-event telemetry actor logged the user
out app-wide (zap gate, etc.).
Fix: "session" is the anonymous telemetry source, not a login identity — only a
managed source (e.g. "nsec") for a different pubkey disqualifies. (Commit
7985a481 intended to allow anonymous actors but only covered the sourceless
shape; the real actor carries source "session".)
tests/app/is-user-logged-in.test.mjs: added the real view-event actor shape
(source "session", different pubkey) → logged in; the "nsec" rejection case still
holds. Mutation-verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Completes todo #15. On login, if an encrypted copy of the user's storage and/or
wallet settings exists on their Nostr account — and this device hasn't already
opted in — offer to restore it.
- js/nostr/encryptedSync.js: exists(dTag) — cheap list-only existence peek (NO
decrypt, so it doesn't spend the signer budget); decryption only on accept.
- storage/walletSyncService: hasRemote() wrappers over exists().
- js/services/settingsRestorePrompt.js: one-time per-pubkey offer. Only prompts
for items with a remote copy not already enabled locally; if nothing remote
exists it does NOT consume the one-time offer (a note published later can still
prompt next login). On accept, pulls + applies.
- app.js handleLoginModalSuccess: fires maybeOfferSettingsRestore(pubkey) after
login, deferred ~2s so relays connect; best-effort, never blocks login; toasts
on restore.
tests/settings-restore-prompt.test.mjs: offer+restore-only-remote-items, decline,
prompt-once, skip-already-enabled, don't-consume-when-empty. Mutation-verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Deleting a video removes the Nostr note(s) but cleanup of the underlying R2/S3
object was silently skipped when storage was locked — leaving the file publicly
downloadable with no indication to the user. Now the delete flow surfaces a
clear heads-up when that happens, telling the user to unlock storage and delete
again (or remove the object from their bucket manually).
To make the signal accurate, deleteVideoStorage now resolves the affected object
keys BEFORE the credentials check (publicBaseUrl is available even while
locked). So "storage-locked" is reported only when there are genuinely hosted
objects to remove; an external-URL video (nothing on the user's bucket) returns
"no-matching-objects" and never triggers a false warning.
Tests (deleteVideoStorage via injected credentials/deleteObject): locked +
hosted object -> storage-locked; locked + external URL -> no-matching-objects
(no false alarm); unlocked -> actually deletes the object. Mutation-verified —
restoring the old credentials-first ordering misreports the external-URL case.
Build + full lint green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Publishing showed a flat "Video shared successfully!" even when most (or all)
relays rejected the event, so users couldn't tell a partial/failed publish from
a clean one. Now publishVideo captures the per-relay accept/reject summary and
stashes a compact {accepted,total} tally on the returned event via a
non-enumerable key (never serialized, never sent to relays). app.js reads it
back and reports the real outcome:
- all relays accepted -> "Video published to N relays!"
- some rejected -> warning "Video published to N of M relays …"
- none accepted -> error (defensive; this path normally throws first)
Falls back to the generic message when no tally is available.
Tests (describePublishOutcome + attach/readRelayPublishSummary): success/partial/
zero/singular/missing-tally messaging, and that the tally round-trips while
staying out of JSON serialization. Mutation-verified — flipping partial→success
and the summary key to enumerable both fail the suite.
Build + full lint green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The For You feed's watch-history suppression and tag-affinity previously
loaded only when the For You / History view was first opened
(feedCoordinator loadLatest({allowStale:true})), so on a cold login the
feed first rendered without suppression and then re-ran ~1-3s later on the
watch-history "fingerprint" event. Watch history was the one feed-driving
list not eagerly warmed in the background on login.
It was deliberately deferred because a cold load used to be ~150+
serialized nip-07 decrypts that starved the single-threaded signer during
the fragile post-login handshake. That cost has since been bounded
(format-routing + newest-first chunk cap + deferred backfill), so the
authSessionCoordinator.js comment's condition for re-enabling an eager
preload is now met.
Kick a fire-and-forget watch-history preload AFTER the blocks/subscriptions/
hashtags lists finish decrypting (never concurrently — same reasoning as the
DM deferral). loadLatest({allowStale:true}) returns immediately and runs the
chunk-capped refresh in the background, so the For You feed's suppression is
warm by the time the user reaches it instead of waiting on the tab.
Extracted as schedulePostLoginWatchHistoryPreload() so the contract is
unit-tested in isolation (correct pubkey + allowStale, pubkey guard,
fire-and-forget / non-blocking, swallows a failed refresh). Build + full
unit suite green; mutation-verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Deleting a video removed the Nostr note but left the actual file (plus its
thumbnail and .torrent) publicly downloadable at its R2/S3 URL forever — a
privacy gap and ongoing storage cost. Now the delete flow also removes the
backing objects, best-effort.
- collectVideoStorageKeys() (storagePointer.js): pure helper that derives the
object keys (video + sibling .torrent + thumbnail) from a note, ONLY for URLs
under the owner's bucket base — external/3rd-party URLs are never returned.
- R2Service.deleteVideoStorage(): resolves the owner's connection, and deletes
those keys. Skips cleanly when storage is locked / no creds / no owned
objects, and never throws (failures are recorded, not propagated) so storage
cleanup can't block note deletion. R2Service class now exported for testing.
- app.handleDeleteModalConfirm: calls it after handleFullDeleteVideo succeeds.
Tests: tests/storage-key-collection.test.mjs (4),
tests/services/r2-delete-video-storage.test.mjs (4). Full suite green.
Note: superseded versions orphaned by past edits aren't retroactively cleaned;
edit-time cleanup (next) prevents new orphans going forward.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The For You feed was chronological, hashtag-interest-EXCLUSIVE, and ignored
follows entirely (docs/feed-algo-audit.md). Now it is inclusive + ranked:
- createForYouScorerStage scores each video: follows-boost (author you follow)
+ interest/watch-topic affinity (cosine vs your interests+watch-history vector)
+ freshness - disinterest penalty. Reuses Explore's vector/similarity helpers.
- createForYouScoreSorter ranks by that score (muted last, recency tiebreak).
- For You pipeline: interests now BOOST not exclude (enforceInterests:false);
scorer added; chronological sorter replaced. Never empty (falls back to
recency when you have no follows/interests/history).
- buildForYouFeedRuntime now feeds subscriptionAuthors + watchHistoryTagCounts.
Watched/blocked/muted are still removed (suppression/blacklist/moderation
stages unchanged). Cheat-resistant tests (older boosted items only win via the
boost). Grid renders end-to-end (~1.3s). Weight tuning is a follow-up.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Real-environment console logs revealed nip-07 login pegging the CPU and
starving encrypted-list decryption via relay REQ storms (~1000 REQ/s):
- moderationService.rebuildTrustedContacts emitted "contacts"
unconditionally, forming an emit->fetch->emit loop (~1600 kind-3 REQ/s).
Now emits only on actual change. Regression test added.
- Per-video live report (kind 1984) and view-count (kind 30079)
subscriptions fan out O(videos x relays) and re-fire on every flaky-relay
reconnect. Circuit-broken behind flags until batched (see refactor plan).
- onVideosShouldRefresh now coalesces forced reloads (2s leading-edge
throttle) so list-change signals can't drive a reload loop.
- nip-07 queue reserves one foreground slot; DM decryption demoted to LOW
so background DM backfill can't starve feed-driving lists. Test added.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The For You feed awaited watchHistoryService.loadLatest() before rendering, and
loadLatest blocked on the full ~1500-item watch-history fetch on a cold load even
with allowStale:true (the `!allowStale || !hasCachedItems` guard awaited whenever
there was no cache). So the main feed froze behind the entire watch-history
resolution, and nothing re-ran the feed once history finished loading — watched
videos were never suppressed and watch-history tags never applied until a manual
refresh.
- watchHistoryService.loadLatest: allowStale callers now NEVER block. On a cold
load the refresh is kicked off in the background and the call returns
immediately with whatever is available (cached items, else the local queue).
- feedCoordinator.subscribeWatchHistoryFeedRefresh: subscribes to the
"fingerprint" event and, when the For You feed is active, debounced-re-runs it
so newly-resolved watched videos are suppressed and tags applied. Wired in at
bootstrap alongside feed registration.
The re-run reads the now-fresh cache (no further refresh scheduled), so there is
no loop. Adds feedCoordinatorWatchHistoryRefresh.test.mjs (debounced single
refresh while active; no refresh when inactive; idempotent subscription).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit replaces the duplicated d-tag extraction logic in `extractDTagValue` method of `js/app.js` with the standard utility function `getDTagValueFromTags` from `js/nostr/nip71.js`. This improves code reuse and maintainability.
The fallback logic for `extractDTagValue` now delegates to `getDTagValueFromTags` if `similarContentController` is not present, ensuring consistent behavior.
Verified with unit tests and linting.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Move `initializeUserInterface()` earlier in `app.init()` to ensure button listeners (like Upload) are bound before potentially slow network operations (Access Control, Relay connection) block or delay responsiveness.
- Make `UploadModal.open()` async and await `load()` to ensure the DOM is ready before removing the `hidden` class.
- Stabilize `notification-system.spec.ts` by adding a wait for app settlement.
- Update visual test baselines.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Updates `.github/workflows/ci.yml` to install `firefox` for the `e2e-headless` job, resolving the "Executable doesn't exist" error.
- Updates `js/app.js` to make `_initAccessControl` async (returning a Promise) and adds a 15-second timeout safeguard. This ensures the app waits for ACLs to load (fixing the race condition) but doesn't hang indefinitely if the network is slow or unresponsive (fixing E2E timeouts).
Co-authored-by: PR0M3TH3AN <[email protected]>
Extracts video modal event handling logic from `js/app.js` to
`js/ui/videoModalController.js`.
- Moves `_bindVideoModalEvents` logic to `VideoModalController.bindEvents`.
- Adds unit tests for the controller.
- Follows AGENTS.md refactor guidelines.
Co-authored-by: PR0M3TH3AN <[email protected]>
The NIP-07 adapter uses type "nip07" but all permission pre-granting
checks only matched type "extension", causing NIP-07 browser extension
users to fail decryption for block/mute lists, subscriptions, DMs, and
watch history. Hashtag preferences appeared to work because their read
path had a separate fix, masking the systemic issue.
Changes:
- shouldRequestExtensionPermissions() now matches both "extension" and
"nip07" signer types (central fix affecting DM signals, comments,
publish helpers, and watch history fetch)
- buildDmDecryptContext() pre-grants extension permissions for NIP-07
- userBlocks.js: remove capabilities-gated decryptor check that failed
with lazy NIP-07 module injection (same fix already in subscriptions.js);
always resolve signer regardless of allowPermissionPrompt; increase
fast-path relay timeout from 6s to 10s
- watchHistoryService: recognize NIP-07 type and always resolve signer
- All publish paths (blocks, subscriptions, hashtags, relays, uploads,
sharing, moderation) now pre-grant permissions for NIP-07 signers
https://claude.ai/code/session_01PipsdaTQmw7wBTJ1LmJjkk
- Implemented `hydrateVideoHistoryBatch` in `NostrClient` to fetch missing root events and sparse histories in a single `pool.list` request.
- Refactored `hydrateVideoHistory` to use shared helper methods `getLocalVideoHistory` and `_processAndCacheEvent`.
- Added `resolveVideoPostedAtBatch` to `PlaybackCoordinator` which delegates to the batched client method.
- Updated `VideoListView` to collect videos needing timestamp resolution and call the batch method, falling back to individual calls if necessary.
- Added regression test `tests/performance/resolvePostedAt.test.mjs` to verify reduced network calls.
Co-authored-by: PR0M3TH3AN <[email protected]>
Added a new configuration variable `ENABLE_NIP17_RELAY_WARNING` to `config/instance-config.js` (defaulting to `false`) to control the display of NIP-17 relay fallback warnings. Updated `js/ui/profileModalController.js` and `js/app.js` to respect this setting.
This change allows instance operators to disable "Privacy warning: direct messages are using your default relays because no NIP-17 relay list is available" and related notifications.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Introduces a `setup()` method in `Application` class to handle complex initialization and side effects.
- Updates `constructor` to be lightweight, only storing configuration options.
- Updates `js/bootstrap.js`, `js/embed.js`, and relevant tests to call `app.setup()` explicitly after instantiation.
- Resolves "Heavy Constructor" code health issue.
- Fixes `tests/video-modal-zap.test.mjs` environment issue with `globalThis.navigator`.
Co-authored-by: PR0M3TH3AN <[email protected]>
Replaced the explicit `typeof` check for `viewLoader` with a simpler assignment using the logical OR operator (`|| null`). This removes excessive defensive coding while preserving the behavior of defaulting to `null` when the argument is undefined or falsy.
Verified with a reproduction test case that confirms `loadView` is correctly assigned or defaulted to `null`.
Co-authored-by: PR0M3TH3AN <[email protected]>
The NIP-07 login flow was extremely slow because list decryption (blocks,
subscriptions, hashtag preferences) started before the signer was ready.
On page refresh, all three services failed with "permission-required" and
scheduled 3-second retry delays, adding 6-10 seconds to every login.
Key fixes:
- List loading now waits for the permission pre-grant to complete before
attempting decryption, ensuring the signer is available on first try
- Extension wait timeout increased from 3s to 5s for slower extensions
- Decrypt timeouts reduced from 20s to 10s (signer is guaranteed ready)
- Auto-login now forwards authType/providerId from saved profiles
- Storage tab unlock falls back to ensureActiveSignerForPubkey when the
global signer is not yet registered
- Watch history feed registration adds defensive re-registration and
try/catch fallback to prevent "Feed not registered" errors
- Decryption scheme cache TTL extended from 10 minutes to 2 hours
- ensureExtensionPermissions waits for extension injection when there
are outstanding permissions to request (was returning immediately)
https://claude.ai/code/session_01T4wwPu7m5qp1Nxx3H2v57j
Removed a duplicate/malformed code block in `js/app.js` that was causing `SyntaxError: Unexpected token '.'` during module loading. This error was breaking tests in `tests/app/channel-profile-moderation.test.mjs` and others.
Also restored `_initNostr` and `_initSessionActor` methods which were partially clobbered or malformed in the file structure.
Verified by running the affected unit tests locally.
Co-authored-by: PR0M3TH3AN <[email protected]>
The `feedCoordinator` logic assumes `this.refreshFeed` exists on the Application instance (because methods are bound via `bindCoordinator`). However, `Application` previously only implemented `refreshRecentFeed`, `refreshForYouFeed`, etc., but not the generic `refreshFeed`.
This caused a `TypeError: this.refreshFeed is not a function` when `loadFeedVideos` (which calls `refreshFeed`) was invoked.
This commit adds `async refreshFeed(...args)` to `Application` in `js/app.js`, delegating to `this._feed.refreshFeed(...args)`.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Removed orphaned code block in `js/app.js` that caused syntax errors during module loading.
- Updated `tests/helpers/moderation-test-helpers.mjs` to implement `resumePendingModeratedPlayback` and decoration logic in `MockApplication`, fixing `tests/app/moderation-overrides.test.mjs` and `tests/app/moderation-settings-refresh.test.mjs` failures.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Restored missing `_initNostr` method in `js/app.js` which was causing `SyntaxError: Unexpected token '.'` and `nostrClient.init is not a function` errors.
- Removed dangling code block that was causing the syntax error (orphaned `accessControl.refresh()...` block).
- This fixes unit tests (e.g. `tests/app/channel-profile-moderation.test.mjs`) and application initialization.
Co-authored-by: PR0M3TH3AN <[email protected]>
Removes a malformed code block that was causing an "Unexpected token '.'" syntax error and restores the `_initNostr` method which correctly initializes the Nostr client.
Fixes: #1083 (SyntaxError in app.js)
Co-authored-by: PR0M3TH3AN <[email protected]>
- Extracted logic into `_init*` private helper methods.
- Preserved execution order and async behavior.
- Cleaned up the main `init` method to act as an orchestrator.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Unify `refreshForYouFeed`, `refreshKidsFeed`, `refreshExploreFeed`, and `refreshRecentFeed` into `refreshFeed(feedType, options)`.
- Update `js/app/feedCoordinator.js` to implement `refreshFeed` and use it in `loadFeedVideos`.
- Update `js/app.js` to delegate specific methods to `refreshFeed`.
- Add `FEED_TYPES` import to `js/app.js`.
- Update `tests/app/feedCoordinator.test.mjs` to match the refactoring.
Co-authored-by: PR0M3TH3AN <[email protected]>
Removes commented-out `await Promise.all` and refactors the associated
`accessControlPromise` and `adminPanePromise` variables in `js/app.js`.
The promises were only assigned for use in the commented-out line.
This change:
- Converts `accessControl.refresh()` to a direct expression statement.
- Simplifies `adminPanePromise` logic into a direct `if` block, removing
the unnecessary `else { Promise.resolve(null) }` branch.
- Preserves the existing fire-and-forget behavior for these initialization tasks.
Co-authored-by: PR0M3TH3AN <[email protected]>
The `loadFeedVideos` method was missing from the `Application` class in `js/app.js`, but was being called by `loadVideos` inside the feed coordinator (which is bound to the application instance). This caused a `TypeError: this.loadFeedVideos is not a function` at runtime.
This change adds the missing delegation method to `Application` in `js/app.js`, forwarding the call to `this._feed.loadFeedVideos`.
Co-authored-by: PR0M3TH3AN <[email protected]>
Replaces the sequential loop in `forceRefreshAllProfiles` with `batchFetchProfiles` to reduce network requests.
Updates `batchFetchProfilesFromRelays` to support `forceRefresh`, ensuring fresh data is requested while maintaining optimistic UI updates.
Adds unit tests for `forceRefresh` behavior.
Co-authored-by: PR0M3TH3AN <[email protected]>