Commit Graph
61 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 4.8 610935ac03 Admin star badge on admin avatars across the app
Show a small gold "verified admin" star in the corner opposite the red
notification dot whenever a rendered profile belongs to a bitvid admin
(the super admin or a current list editor).

- adminBadge.js (new): isAdminActor(hex|npub) sync check against
  accessControl, and applyAdminStar(container, id) which adds/removes a
  single `.admin-star` in a non-clipped, positioned container. Idempotent.
- CSS: `.admin-star` (top-left gold disc, mirrors the top-right dot),
  `.dm-avatar-wrap`, and position:relative on the comment avatar wrapper.
- Profile button: app.refreshAdminStar() lights the logged-in user's star;
  re-runs on login/logout and on accessControl.onEditorsChange (so a
  late-loading editor list still resolves).
- Comments: star on comment-author avatars (commentNodeFactory).
- Channel page: wrap the clipped avatar and star the channel owner.
- DMs: Avatar gains an opt-in adminId — only admin avatars get wrapped so
  non-admin rows are byte-identical; ContactRow passes the peer pubkey.
- tests: admin-badge unit coverage (recognition, idempotency, removal).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 10:10:19 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ef4191538c Profile dot: show for DMs OR pending admin submissions
The profile-button red dot now lights when the logged-in user has either
unseen DMs or (if they can edit admin lists) pending submissions to review.
The Messages-nav dot stays DM-only.

- applicationBootstrap.js: split indicator state into _dmUnread and
  _submissionsPending; applyProfileNotificationDot() ORs them for the
  profile dot. Added refreshSubmissionsIndicator (gated on FEATURE_SUBMISSIONS
  + canEditAdminLists), a bitvid:submissions-changed listener, a 180s poll,
  and a startup refresh.
- authSessionCoordinator.js: refresh the submissions indicator on
  auth-login and auth-logout alongside the DM refresh.
- submissionsSection.js: emit bitvid:submissions-changed after approve/deny
  so the dot recomputes for all admins.
- check-file-size.mjs: bump authSessionCoordinator grandfather baseline.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 09:35:41 -04:00
thePR0M3TH3ANandClaude Fable 5 8d45f25962 zaps: make the card + modal zap counter always visible (0 sats when none)
Diagnosis (live, verified): the zap-totals pipeline is correct end to end —
a seeded receipt queries and sums to the right sats — but of 310 kind-9735
receipts reachable on the relays, ZERO reference any bitvid video, so every
badge was legitimately hidden. That reads as "broken" and profile zaps to a
Lightning address never attach to a video, so hidden-when-zero left the
feature invisible in practice.

Show the counter always, like the view counter it sits beside: "0 sats"
until a receipt lands, then the summed total. Card binder and modal render
no longer gate on sats>0; the badge is revealed on bind/open and re-hidden
on modal close (no video open). Live-checked: 158/158 cards render the
badge. Tests spec-corrected with integrity notes.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 14:43:22 -04:00
thePR0M3TH3ANandClaude Fable 5 213cb97ee8 modal: fix video player failing to open — missing-delegate TypeError on every open
Root cause of "clicking a video card no longer opens the player":
coordinator methods run with `this` bound to the Application
(bindCoordinator), and the zap-badge helpers were added as coordinator
methods called via `this.subscribeModalZapTotal(...)` — but app.js only
exposes coordinator methods through explicit delegates, which were never
added. Every modal open threw "this.subscribeModalZapTotal is not a
function" inside subscribeModalViewCount (and every close threw in
teardown). Same delegate landmine that broke Most Zapped's loader.

The helpers are now plain closures inside createModalCoordinator — immune
to the delegate pattern by construction. Regression test drives the REAL
bindCoordinator with only app.js's actual delegates, so any future
cross-method `this.` call without a delegate fails in CI first.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 10:45:50 -04:00
thePR0M3TH3ANandClaude Fable 5 cbe894969d zaps: fix badge never showing (array pointers) + zap total in the player modal
The zap-totals store's pointerKey only understood {type,value} objects, but
deriveVideoPointerInfo produces tag-style ARRAYS (["a", value, relay?]) — so
every card-badge lookup keyed to nothing, no receipt fetch was ever
scheduled, and the Most Zapped ranking metric silently read 0 for every
video. pointerKey now normalizes both shapes; tests use the array shape the
app actually passes.

Also adds the orange sats badge to the video player modal's meta row
(right side via the row's existing justify-between; views stay left),
driven by the same cache/change-signal as the cards and torn down with the
view-count subscription.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 09:20:11 -04:00
thePR0M3TH3ANandClaude Fable 5 8dfd2034b0 feeds: "Most Zapped" sidebar tab ranked by zap totals (#47)
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]>
2026-07-03 21:38:53 -04:00
thePR0M3TH3ANandClaude Opus 4.8 4795d5edbd [auth] Bug pass on login/signer/session surface: 4 fixes before promotion
Requested pre-promotion sweep of login methods, active signers, and anon session
actors. Four real bugs found and fixed:

1. Logging out could wipe EVERY saved account's remembered nsec key. The no-arg
   clearStoredSessionActor() removed the whole per-account v2 map, and it was
   reachable from SignerManager.logout() (non-persisted nsec cleanup) and from
   the blocked-key cleanup in registerPrivateKeySigner — same bug class as the
   NIP-46 session-map wipe fixed earlier. Now: the no-arg form clears only the
   legacy v1 slot; logout targets the departing account (and skips entirely when
   it can't name one); the blocked-key cleanup targets the blocked pubkey
   (mirroring the already-targeted twin in unlockStoredSessionActor).
   Spec-corrected the test that had encoded the wipe-all as intended behavior.

2. Removing a saved profile left its keys on the device. The switcher's
   remove-account flow cleared NWC + the keep-unlocked cache but kept the
   remembered (encrypted) nsec key and the NIP-46 session. New
   client.forgetStoredAccount(pubkey) (SignerManager.forgetStoredSessionActor +
   clearStoredNip46Session) forgets both WITHOUT touching the active session's
   in-memory actors; wired into handleProfileLogoutRequest.

3. Logout never actually re-locked a kept-unlocked device. The forget read
   detail.pubkey / this.pubkey — both already nulled by authService.logout()
   (the detail only carries previousPubkey) — so the cached decrypted key
   silently survived logout. Now reads previousPubkey.

4. restoreUnlockedSigner bypassed access control: a since-blacklisted account's
   cached key would silently restore its signer. It now accepts a validator
   (mirroring unlockStoredSessionActor), forgets the cached key on denial, and
   both app-level call sites (_initAutoLogin, ensureEncryptionCapableSigner)
   pass an accessControl.canAccess gate.

Also verified-safe during the pass (no action): logout's persisted-nsec keep
logic under the ephemeral session-actor overwrite; NIP-46 logout keepStored;
the ephemeral actor as a harmless extra DM-decrypt candidate; corrupt-map
recovery clears; anon blacklist-seeding now always has a signing-capable actor.

Tests: session-actor multi-account suite spec-corrected + re-verified (no-arg
clear keeps per-account keys); 57 tests across session-actor / nip46 /
unlocked-key / switch / signer-gate / feed-gate suites green. Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 21:59:52 -04:00
thePR0M3TH3ANandClaude Opus 4.8 b8e6921ac0 [perf] Stale-while-revalidate cold boot: don't serialize behind admin lists + list sync
Cold refresh stalled on "Fetching moderation filters…" and then (logged in)
"Fetching subscriptions…" before the grid could render. Boot was SERIAL behind
relay fetches even though every stage already had a local cache:

1. _initAccessControl awaited the full admin-list relay refresh (up to 15s)
   before auto-login — but accessControl hydrates whitelist/blacklist from
   localStorage at construction, and isHydrated() existed for exactly this case
   (unused). Now: when cached lists are present (every visit after the first),
   boot proceeds immediately and the refresh continues in the background — the
   existing whitelist/blacklist change listeners re-filter the grids when it
   lands. First-ever visits still wait (a whitelist-mode instance must never
   render unfiltered). The admin-pane refresh no longer blocks boot at all.

2. bootstrapTrustedSeeds awaited the same relay refresh (up to 3.5s more).
   waitForAccessControl now fast-paths on isHydrated(): cached editors seed
   trust immediately, and its onEditors/Whitelist/BlacklistChange listeners
   already re-apply seeds when the real refresh lands.

3. The post-login feed gate held the grid for the FULL list sync + NIP-07
   decryption (up to 8s). New js/app/feedListCacheGate.js: a probe that checks
   all three list services (blocks/subscriptions/hashtags) hold a valid cache
   for the active pubkey, raced into the gate — warm boots open as soon as the
   caches hydrate (first ticks) and the background sync re-filters on arrival.
   Cold boots (no caches) behave exactly as before.

Net effect: warm cold-refresh renders from IndexedDB-cached events with cached
filters/lists near-instantly; relay truth streams in behind it. No moderation
semantics changed — every stage that lacks a cache still waits.

Tests: tests/feed-list-cache-gate.test.mjs (5 — all-three-caches probe incl.
stale-pubkey/missing-service, late-hydration early release, cold-boot timeout,
throwing probe). Access-control + switch suites green. Lint + build green
(coordinator compacted to stay under its size cap).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 15:32:34 -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 298e456ff9 [dm] Clear the previous account's DMs on account switch (not just the modal view)
Switching accounts (e.g. NIP-07 → nsec) still showed the OLD account's direct
messages. The earlier switch fix reset only the profile modal's DM view, but the
DM data lives in the app-wide nostrService store (this.dmMessages), which the
messages pane reads via listDirectMessageConversationSummaries /
getDirectMessagesForConversation. Logout clears that store
(stopDirectMessageSubscription + clearDirectMessages); the switch path never did,
so the old messages persisted.

Fix: on an identity change, authSessionCoordinator.handleAuthLogin now stops the
old DM subscription and clears the in-memory DM store BEFORE the new account's
subscription starts — so the pane shows the new account's DMs (or empty→new),
never the old.

Uses a new clearDirectMessages({ keepSnapshot: true }) option: it wipes the
in-memory store but preserves each account's PERSISTED per-actor DM snapshot, so
switching back to an account stays fast (per-account DM cache). Logout still fully
clears (keepSnapshot defaults false).

Tests: tests/services/nostr-service.test.mjs — clearDirectMessages empties the
in-memory store + resets the active actor and emits cleared/updated, in both
keepSnapshot modes. Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 21:45:23 -04:00
thePR0M3TH3ANandClaude Opus 4.8 1b17bb8fcc [auth] Opt-in "keep unlocked" so a reload doesn't re-prompt for the PIN (#51)
After unlocking a saved nsec key the user had to re-enter the PIN on every reload,
because the key is only passphrase-encrypted and the in-memory signer is lost on
refresh. Add an opt-in unlocked-key cache with two tiers (maintainer's choice):

- session (default, always): sessionStorage — survives refresh/navigation, cleared
  when the tab/browser closes. Same rough exposure as the in-memory signer.
- persistent (opt-in): the unlock prompt now shows a "Keep me unlocked on this
  device" checkbox with an inline unencrypted-at-rest warning; when checked the key
  is also written to localStorage and survives until site data is cleared.

client.restoreUnlockedSigner(pubkey) re-registers the signer from the cache WITHOUT
a passphrase — it verifies the cached key derives to that pubkey and forgets a
mismatch, so it never adopts another account. It runs proactively from
ensureActiveSignerForPubkey (auto-login/refresh) and lazily from
ensureEncryptionCapableSigner before any prompt, so after a reload the signer is
restored silently and storage/hashtags/DMs/reactions/blocks/subs all just work.
Logout and remove-saved-profile forget the cached key (forgetUnlockedSigner).

SECURITY: while cached, the key is stored decrypted — the passphrase no longer
protects it at rest. Session tier is the default; persistence is opt-in + warned.

Tests: tests/unlocked-key-cache.test.mjs (8 — session vs persistent tiers, promote
persistent->session on read, reject non-hex, forget one, clear all, missing-storage
degrade). Related unlock/switch/session-actor suites green. Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 19:51:41 -04:00
thePR0M3TH3ANandClaude Opus 4.8 1622e85d10 [auth] Route reactions, blocks, subscriptions & DM send through the signer gate (#54/#57)
Fast-follows to the ensureEncryptionCapableSigner gate: every remaining
signing/encryption flow now re-unlocks a reloaded nsec signer with one passphrase
prompt instead of dead-ending. Each call site short-circuits ONLY on the
user-driven outcomes (cancel / bad-passphrase); every other case falls through to
the flow's own existing error handling, so non-nsec signers (e.g. an unresponsive
NIP-07 extension) behave exactly as before.

- Reactions (#54): ReactionController.handleReaction gates before the optimistic
  update with need:"sign" (kind-7 reactions don't need encryption) — fixes the
  "like/dislike does nothing" that was a rolled-back publish on a lost signer.
- Blocks: handleProfileBlocklistMutation (need:"encrypt", NIP-04 mute/block lists).
- Subscriptions: the channel Subscribe/Unsubscribe toggle (need:"encrypt", kind 30000).
- DM send: both composers — handleSendProfileMessage and handleDmAppShellSendMessage
  (need:"encrypt", NIP-17/NIP-04).

Reactions get the gate via a new ensureSigner callback on ReactionController; the
modal flows use the services.ensureEncryptionCapableSigner already exposed.

Tests: tests/reaction-signer-gate.test.mjs (3 — cancelled/bad-passphrase block
publish, ok proceeds, asserts need:"sign"). Related suites (decision, DM, profile
modal, switch delegators) green. Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 19:23:04 -04:00
thePR0M3TH3ANandClaude Opus 4.8 87af650b38 [profile] Refresh the profile modal on account switch (no manual reload)
Switching accounts with the profile modal open (e.g. NIP-07 -> a saved nsec) left
the PREVIOUS account's DMs, subscriptions, hashtags and lists on screen until the
user refreshed. Two problems:

1. handleAuthLogin ran its identity-reset steps unguarded, so if any one threw
   (renderSavedProfiles, a warm-list call, etc.) the steps after it were skipped —
   including handleActiveDmIdentityChanged, which is what clears the previous
   account's conversation. Result: stale DMs after a switch. Each reset step now
   runs in isolation via runIdentityResetStep, and the DM reset runs first.

2. The switch-with-modal-open refresh called populateOpenProfilePanels(), which
   only paints from current service state and omits lazily-loaded panes (messages,
   watch history). It also fired before the new identity's lists had loaded, with
   no reliable refresh afterward. Replaced with refreshOpenPanesForActiveIdentity(),
   which re-runs the ACTIVE pane through selectPane() — the same load+populate path
   as opening it fresh — so DMs reload, subscriptions/hashtags refetch, etc. It runs
   immediately, again once profile/wallet data settles, and once more from the auth
   coordinator when the new identity's blocks/subscriptions/hashtags finish loading
   (backfill for slow sync). Non-active panes stay lazy and repopulate on select.

Per-user list caches (blocks/subscriptions/hashtags are already keyed per pubkey)
mean the active pane shows cached data instantly on switch, then updates when the
new identity's fetch settles — no manual refresh.

Tests (tests/profile-modal-controller.test.mjs +3): a throwing reset step is
isolated; a throwing DM reset no longer skips the saved-profiles reset; and
refreshOpenPanesForActiveIdentity re-runs the active pane only when the modal is
open. Existing profile-modal / switch-coordinator / stored-nsec-switch suites green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 16:31:01 -04:00
thePR0M3TH3ANandClaude Opus 4.8 8692dac671 [nostr-core] Per-account nsec key storage for multi-account switching (#48)
Move the encrypted-key store from a single slot to a per-pubkey map so
several saved nsec accounts each keep their own key on the device and the
profile switcher can move between them. Previously bitvid:sessionActor:v1
held one key (last-saved wins), so switching among saved nsec accounts
silently failed.

- sessionActor.js: new bitvid:sessionActors:v2 map keyed by pubkey (follows
  the existing bitvid:<thing>:<npub> convention). readStoredSessionActorEntry,
  persistSessionActor, clearStoredSessionActor take an optional target pubkey;
  legacy v1 slot is kept in sync as the last-saved default and migrated into
  the map on first read. IndexedDB mirror keyed per-pubkey. Adds
  listStoredSessionActorPubkeys().
- client.js: getStoredSessionActorMetadata / unlockStoredSessionActor accept a
  target pubkey; access-denial cleanup forgets only the blocked account.
- nsec provider threads expectPubkey into the unlock so a switch unlocks the
  requested account, with the expectPubkey guard as a backstop.
- authSessionCoordinator: look up the target account's stored-key metadata.

NIP-46 multi-account (single slot bitvid:nip46:session:v1) remains as a
follow-up. Tests: tests/session-actor-multi-account.test.mjs (7).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 22:33:03 -04:00
thePR0M3TH3ANandClaude Opus 4.8 abdd11b18c [auth] #48: NIP-46 switch reconnects stored session + reject wrong-account switches
Completes fluent switching across signing methods (NIP-07 / nsec / NIP-46):

- NIP-46 switching: it had the same bug — switchProfile called the provider with no
  reuseStored, so it started a fresh handshake (needs a new connect URI/QR) instead of
  reconnecting. Now handleProfileSwitchRequest sends { providerId: "nip46",
  reuseStored: true } for NIP-46 targets; authService.switchProfile forwards reuseStored;
  the nip46 provider's useStoredRemoteSigner reconnects the stored session and activates
  the account. On failure -> a clear "reconnect your remote signer" toast.

- Wrong-account guard (all methods): requestLogin now enforces expectPubkey (only set by
  switchProfile). A single-slot stored nsec/NIP-46 session resolving to a DIFFERENT
  account than requested is rejected with `pubkey-mismatch` instead of silently switching
  to the wrong profile. Also protects NIP-07 (switching to an account the extension isn't
  currently on fails clearly instead of grabbing the extension's active account).

Tests: tests/authService.test.mjs (+3 — unlockStored/passphrase + reuseStored forwarding,
expectPubkey rejection). Build + lint clean; auth suites green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 22:19:13 -04:00
thePR0M3TH3ANandClaude Opus 4.8 fb2f12b49d [auth] #48: fluent switch to a saved nsec account (prompt for PIN + unlock stored key)
Switching to a saved nsec profile silently failed: switchProfile -> requestLogin -> the
nsec provider was called with no passphrase, so it threw "secret-required" and the switch
was caught+logged with no user feedback. It never prompted for the PIN.

Now handleProfileSwitchRequest detects an nsec target and, before switching, prepares the
stored-key unlock:
- evaluateStoredNsecSwitch(meta, target) (pure) gates on a persisted nsec key for THIS
  account (via getStoredSessionActorMetadata); errors clearly otherwise ("log in with its
  nsec", "different account's key saved").
- showPasswordPrompt (js/ui/promptDialog.js) collects the PIN/passphrase in a styled
  dialog (Promise<string|null>, stacks above modals).
- switchProfile(pubkey, { unlockStored: true, passphrase }) — authService.switchProfile
  now forwards unlockStored/passphrase to requestLogin, which routes to the nsec
  provider's unlockStoredSessionActor to restore the signer and activate the account.
- Wrong PIN -> "Incorrect PIN/passphrase" toast (decrypt-failed).

So NIP-07 -> nsec switching works fluently. NIP-46 switching is unchanged (follow-up to
verify/extend); multiple-nsec accounts still share one stored slot (follow-up).

Tests: tests/stored-nsec-switch.test.mjs (6), tests/password-prompt.test.mjs (6). Build +
lint clean; auth suites 25/25.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 22:11:43 -04:00
thePR0M3TH3ANandClaude Opus 4.8 8ab531b241 fix(access): canAccess cross-derives hex+npub so whitelisted authors aren't blocked (#32)
A video from a genuinely whitelisted author refused to play with "not from a
whitelisted author" (the source URL was confirmed healthy — HTTP 200, video/mp4,
range, CORS *). Root cause: accessControl keeps the whitelist in two sets —
whitelistPubkeys (hex) and whitelist (npub) — and canAccess consulted only one
based on the caller's input format without cross-deriving. playbackCoordinator
passes the author's npub, so it checked only the npub set; an entry present in
only the hex representation read as "not allowed."

- canAccess now derives BOTH hex and npub from the candidate and checks both sets
  (only fixes false-negatives; a non-whitelisted author is still denied).
- Dev-gated diagnostic at the playback rejection logs hex/npub, whitelistMode,
  set sizes, and inNpubSet/inHexSet to pinpoint any remaining denial.
- Tests: tests/access-control-canaccess.test.mjs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 10:39:11 -04:00
thePR0M3TH3ANandClaude Opus 4.8 0a37e625bb playback: fail over across hosted mirrors before dropping to P2P (#5)
The liveness probe already tries all of a video's sources, but actual playback
only ever tried the single primary URL and then fell straight to WebTorrent. Now
playback fails over across hosted mirrors first.

- PlaybackSession builds an ordered, de-duped hostedSourceCandidates (primary URL
  first, then the video's imeta `sources`; accepts string or {url} entries).
- attemptHostedPlayback takes a candidateUrl; the url-first execution loop tries
  each candidate and advances to the next mirror on probe-fail/stall/timeout,
  cleaning up watchdogs + resetting the <video> between candidates, only calling
  WebTorrent after every hosted mirror fails.
- `sources` plumbed coordinator -> strategy -> session. Single-source videos
  (no `sources`) behave exactly as before.

Tests: tests/services/playbackService_multisource.test.mjs (candidate
construction; the full async fail-over flow is covered by the existing
playbackService_order suite in CI — that mock-timers harness doesn't run in this
sandbox). ROLLBACK: revert the loop to the single attemptHostedPlayback() call
and drop the sources plumbing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 13:30:30 -04:00
thePR0M3TH3ANandClaude Opus 4.8 04621efc52 feed: stop the grid re-sorting ranked feeds chronologically; Trending defaults
The real reason every tab looked identical: VideoListView.render re-sorts
displayVideos by date UNLESS metadata.preserveOrder is set, and only Explore set
it. So For You's tiering, Trending's view-count ranking, and Kids' scoring were
all discarded at render and shown in plain chronological order — i.e. the #21
feed-identity work was invisible. Set preserveOrder for For You, Trending, and
Kids so their feed-engine order survives to the grid.

Also per request:
- Sidebar: Trending now sits above Recently Added.
- Default homepage: logged-out users land on Trending (gated by
  FEATURE_TRENDING_FEED, falls back to Recent); logged-in stays For You. The
  logout-from-For-You redirect follows the same rule.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 13:13:45 -04:00
thePR0M3TH3ANandClaude Opus 4.8 02854d4fb5 feed: add Trending tab — recently-added ranked by view count (#27)
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]>
2026-06-24 13:06:34 -04:00
thePR0M3TH3ANandClaude Opus 4.8 087cf410cb feed: make Explore "new-to-you + diverse, less recency"
Explore shared the full active set with Recent/For You and leaned on freshness,
so it read like a reshuffled Recently-added. Give it a discovery identity:

- Weights: drop freshness (0.25 -> 0.12) and bump novelty (0.30 -> 0.35) so
  recency stops dominating and topic-novelty leads.
- Add a soft followed-author penalty (weight 0.3): the scorer now reads
  subscriptionAuthors from the Explore runtime and deprioritizes creators you
  already follow, so non-followed creators surface ("new to you"). A penalty,
  not a filter, so the feed stays full if you follow lots of people.
- More variety: lower the diversity sorter's MMR lambda (0.7 -> 0.5).

Also compacted For You's subscriptionAuthors reader to a one-liner to stay
under feedCoordinator's size cap while adding the same reader to Explore.

Tests: explore follows-penalty scoring; existing explore-scorer/diversity +
feedCoordinator suites green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 08:50:50 -04:00
thePR0M3TH3ANandClaude Opus 4.8 065706a3cf videos: cross-ecosystem dedup (prefer bitvid, show one) across all grids
Phase 1 of the on-boarding plan, and the shared layer for unified grid
loading. A video that exists as both a bitvid kind-30078 note and a NIP-71
mirror/original previously could appear twice (e.g. independent dual-posts).

js/utils/videoDeduper.js: collapseCrossEcosystem() groups events by a
per-author identity (fileSha256 / originalFileSha256 / infoHash, plus an
`eid` namespace that links an on-boarded note's `importedFrom` to the
original foreign event) via union-find, and keeps the bitvid-native version
(else newest). dedupeVideos() = root-dedup THEN cross-ecosystem collapse.

Routed through the shared chokepoint app.dedupeVideosByRoot (playback
coordinator), so the feed stage, channel profile, and search all collapse
duplicates identically — "show one, prefer bitvid" everywhere. Channel
fallback path updated too.

Limitation (documented): hash-less independent dual-posts can't be matched
until the bitvid side carries on-board provenance (Phase 2).

Tests + mutation-verified (prefer-bitvid, per-author identity, infohash,
provenance link, root+cross composition). 1262/1263 unit (known flake).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 19:51:43 -04:00
thePR0M3TH3ANandClaude Opus 4.8 512b6c3f6a feat(feed): warm watch history in background after login
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]>
2026-06-19 08:04:54 -04:00
thePR0M3TH3ANandClaude Opus 4.8 006403b7b4 [playback] stop the WebTorrent webseed flood on multi-file torrents
The video's hosted CDN URL was added to WebTorrent's `urlList`, but a
standalone hosted-file URL is only a valid BEP19 webseed for a SINGLE-file
torrent. For a multi-file torrent WebTorrent treats it as a base directory and
appends "<torrent name>/<file path>", producing a doubled URL that 404s on
every request — the flood seen on e.g. archive.org videos.

The CDN URL is no longer placed in urlList. It's forwarded separately as
`hostedUrlWebSeed` and attached post-metadata (handleTorrentStream) via
torrent.addWebSeed ONLY when the torrent is a single file, where the URL maps
directly to the file. Multi-file torrents get no file-pointing webseed, so no
flood. Magnet-declared `ws=` webseeds are unaffected (passed via urlList as
before). With CDN-first, dual-source videos play the CDN directly anyway, so
this only affects forced-torrent / URL-less paths.

Tests: a single-file torrent receives the hosted URL via addWebSeed; a
multi-file torrent does not. The single-file guard is mutation-verified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 16:05:54 -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
thePR0M3TH3ANandClaude Opus 4.8 71cd04b215 perf(login): stop eagerly loading DM history at login
Real-env console (cold nip-07 login) showed list decryption failing en
masse: blocks/hashtags/subscriptions fetched fine (relay cap working —
every sub now fans out to exactly 8 relays) but decryption timed out for
minutes with "message channel closed", only recovering on a page refresh.

Root cause: the single-threaded nip-07 extension channel is overwhelmed
during the post-login handshake. One contributor is the eager DM load —
loadDirectMessages({ limit: 50, initialLoad: true }) ran at login and
fired a burst of decrypts that competed with the feed-driving lists
(blocks/subscriptions/hashtags) for the same channel. DMs are lower
priority (the user isn't viewing Messages at login) and already load
lazily when the Messages tab opens.

Defer the eager DM load; report dms state as "deferred". The lightweight
unread-indicator refresh still runs so the badge stays accurate. First
increment of the "cut the login burst" work (KNOWN_BUGS #0).

Test: tests/app/login-defers-dms.test.mjs (cheat-resistant — asserts
loadDirectMessages is never called at login, dms state is "deferred", and
the unread indicator still runs).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 23:16:40 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c0a8636678 feat(feed): make For You inclusive + ranked by your lists (G1-G5)
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]>
2026-06-14 16:57:05 -04:00
thePR0M3TH3ANandClaude Opus 4.8 9c3ee01755 fix(feed): reload only when the block set changes (P5; KNOWN_BUGS #2)
handleBlocksLoaded reloaded the feed on every 'blocks-loaded' signal — but a
forced reload re-fetches the lists, which re-emit 'blocks-loaded', a
self-sustaining loop. Now it computes a signature of the block set and reloads
only on actual change (emit-on-change — the P5 principle). handleRelaysLoaded
only touches relay UI; hashtag prefs already dedup by signature.

Cheat-resistant regression test added. Bumped authSessionCoordinator file-size
baseline. Logged 2 pre-existing app test failures (fail at pre-session baseline)
in KNOWN_BUGS. Marks KNOWN_BUGS #2 resolved.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 16:25:42 -04:00
thePR0M3TH3ANandClaude Opus 4.8 dde0c35477 refactor(views): batch kind-30079 view counts via SubscriptionManager (P3)
Replaces the per-card view-count transport (each card: list + NIP-45 COUNT +
live sub, × relays — the ~880 REQ/s storm) with ONE shared batched kind-30079
subscription + ONE backfill list() over all visible cards, bucketed back to
per-pointer state by #a. Re-enables the VideoListView circuit-breaker.

Grid counts are convergent/approximate (cheap, persists via the existing view
cache); the watch page opts into an exact NIP-45 COUNT for its single pointer
(modalCoordinator passes { exact: true }) — accurate where it matters, one
request, no storm. Legacy per-pointer path retained for SubscriptionManager-less
environments (tests/mocks).

Verified: nip07-heat steady-idle with views on; flaky multi-relay harness still
all-PASS; new cheat-resistant batching+bucketing test. (Pre-existing
view-counter publish-session-tag failure is unrelated — fails at baseline.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 15:49:50 -04:00
thePR0M3TH3ANandClaude Opus 4.8 3d0564feea fix(auth): stop eagerly preloading watch history on login
The eager watch-history preload (0c8a17e3) — even deferred after the critical
lists — saturated the serialized nip-07 extension with ~150+ chunk decrypts for
~18s after login. If the profile modal was opened in that window, its hashtag /
subscription / DM loads competed for the extension and failed/timed out, and DMs
felt broken.

Revert to lazy loading: watch history loads when the For You feed or History view
is opened, keeping login light so the critical lists and the modal work. Re-add
an eager preload only after the cold-load decrypt cost is reduced (task #14).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 20:32:03 -04:00
thePR0M3TH3ANandClaude Opus 4.8 5a45ec7bf5 fix(auth): defer watch-history preload so it doesn't starve the critical lists
Regression from the eager watch-history preload (0c8a17e3): it fired BEFORE the
blocks/subscriptions/hashtags list sync. The nip-07 extension serializes all
decrypt calls, and a cold watch history is ~150+ chunk decrypts — so those
queued ahead of the small critical lists, which then timed out ("hashtags not
working / subscriptions super slow") while watch history slowly drained the
extension (~20s).

Move the preload to AFTER Promise.all(parallelListTasks) so the small, critical
lists decrypt first, then watch history warms in the background. Still
non-blocking; still emits "fingerprint" for the For You re-run.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 20:06:57 -04:00
thePR0M3TH3ANandClaude Opus 4.8 8c4412a562 perf(watch-history): cache decrypted chunk plaintext by event id
Watch history is ~150 separately encrypted chunk events; decrypting each one is
a serialized round-trip to the nip-07 extension (the key lives in the extension
and cannot be cached locally), so a full re-decrypt cost many seconds on EVERY
load. Chunk events are addressable/replaceable: an unchanged chunk keeps its
event id while a modified chunk republishes under a new id.

Add a persisted cache (watchHistoryDecryptCache.js) keyed by chunk event id. The
fetch path checks it before calling the extension and stores successful
decryptions, so reloads decrypt only the chunk(s) that actually changed
(typically one) and skip the extension for the rest — across sessions. Bounded
to 400 entries; cleared on logout alongside the conversation-key cache. The
cached plaintext is no more sensitive than the watch-history items already
persisted by the manager.

Adds watchHistoryDecryptCache.test.mjs (round-trip, persistence/re-hydrate,
bounds, clear).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 15:07:18 -04:00
thePR0M3TH3ANandClaude Opus 4.8 0c8a17e3cf perf(watch-history): eagerly preload on login (was lazy until For You/history tab)
Watch history was never loaded as part of the login cascade — it only loaded
when the For You feed refreshed or the history tab was opened. So on login (and
on other feeds) watched-video suppression and watch-history tags did nothing
until the user navigated there.

Kick off a non-blocking watchHistoryService.loadLatest() during the login
list-sync (deliberately NOT in parallelListTasks, so the feed never waits for
it). loadLatest schedules the background refresh, which emits "fingerprint" and
the For You feed re-runs (feedCoordinator) to apply suppression once resolved.
Deduped against the feed's own loadLatest via the in-flight refresh promise.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 14:18:09 -04:00
thePR0M3TH3ANandClaude Opus 4.8 52ceb4f211 perf(watch-history): load in background; re-run For You when it resolves
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]>
2026-06-12 11:44:01 -04:00
thePR0M3TH3ANandClaude Opus 4.8 8850eb962a perf(watch-history): memoize NIP-44 conversation key across chunk decrypts
Watch history is stored as up to ~150 separately NIP-44-encrypted chunk
events. Each chunk built a fresh cipher suite whose conversation-key cache
lived only in its own closure, so the ECDH conversation key (~12ms each in
the bundle) was re-derived once per chunk — multiplied across actor-candidate
and scheme-fallback retries this synchronously blocked the main thread for up
to ~20s after login/profile open (the reported freeze + fan spin-up).

Memoize the derived key by (scheme, privateKey, target) at module scope so it
is computed once per load and reused. Cleared on logout/identity change next
to clearDecryptionSchemeCache so a derived shared secret never outlives its
session. On the 150-chunk path this drops decrypt time ~1955ms -> ~182ms.

Adds tests/nostr/watchHistoryConversationKeyCache.test.js covering: 150 chunks
=> one derivation, distinct identities derive independently (no cross-identity
reuse), and cache-clear forces re-derivation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 09:41:06 -04:00
thePR0M3TH3AN c23dcc6c46 [e2e] Improve runtime coverage and fix logout coordinator call 2026-02-22 20:44:31 -05:00
google-labs-jules[bot]andPR0M3TH3AN c11afb7395 Fix lint failure: Remove direct style.display usage in uiCoordinator
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-22 15:22:54 +00:00
google-labs-jules[bot]andPR0M3TH3AN 5c944199d9 Fix E2E test failures by enforcing relay isolation and disabling whitelist mode
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-22 14:32:54 +00:00
google-labs-jules[bot]andPR0M3TH3AN 1e2ce6bf61 Fix E2E login flows and test harness relay isolation
Addresses failures in login-flows.spec.ts by:
1. Updating the test to correctly interact with the provider-based login modal (clicking the 'nsec' button).
2. Patching the test harness `relayManager` to prevent overwriting test relays with production defaults during login/logout cycles.
3. Hardening `uiCoordinator` to ensure visibility assertions pass reliably by enforcing `style.display` properties.
4. Explicitly initializing test relays in `bitvidTestFixture` to guarantee environment isolation.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-22 13:48:19 +00:00
thePR0M3TH3AN 5d4a28ef3a update 2026-02-21 16:21:57 -05:00
google-labs-jules[bot]andPR0M3TH3AN 2aae4094e7 [constants] refactor duplicate numeric constants
- Add `UI_FEEDBACK_DELAY_MS`, `DEBOUNCE_DELAY_MS`, and `NETWORK_RETRY_DELAY_MS` to `js/constants.js`.
- Replace magic numbers (5000, 2000, 1500) with semantic constants in:
  - `js/ui/notificationController.js`
  - `js/ui/profileModal/ProfileDirectMessageActions.js`
  - `js/ui/urlHealthController.js`
  - `js/app/uiCoordinator.js`
  - `js/ui/components/VideoModal.js`
  - `js/ui/components/UploadModal.js`
  - `js/services/dmNostrService.js`
  - `js/services/hashtagPreferencesService.js`
  - `js/app/feedCoordinator.js`
  - `js/services/moderationService.js`

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-12 03:43:03 +00:00
google-labs-jules[bot]andPR0M3TH3AN b28e62963f perf: Batch video timestamp resolution to reduce N+1 network requests
- 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]>
2026-02-11 12:01:39 +00:00
google-labs-jules[bot]andPR0M3TH3AN c47392aba6 Refactor numeric constants to centralized module
- 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]>
2026-02-11 03:27:26 +00:00
google-labs-jules[bot]andPR0M3TH3AN 0da9401235 Refactor playback selection strategy into dedicated service
Extracted the complex playback logic from `js/app/playbackCoordinator.js` into a new `js/services/playbackStrategyService.js`.
This improves maintainability by separating the orchestration logic from the application glue code.
The coordinator now delegates to the service, passing a context object for UI interactions and state synchronization.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 04:27:31 +00:00
Claude 29215d9b5e Speed up Nostr login and fix list sync, storage unlock, and feed registration
- Remove redundant double signer readiness gate evaluation that added 5-10s
  to the login path (authSessionCoordinator)
- Reduce decryption timeouts (10s→6s per scheme, 3s→1.5s retry delay,
  8s→5s NIP-07 no-prompt, 45s→25s overall ensureLoaded) for faster scheme
  fallback during list sync
- Fix storage tab unlock: remove overly strict capabilities check that
  blocked lazy-injecting NIP-07 extensions, and add auto-unlock attempt
  when profile modal opens with an active signer
- Fix subscription/hashtag decryption: always resolve signer for background
  refreshes (was previously gated behind allowPermissionPrompt), and always
  check window.nostr as fallback decryptor regardless of prompt mode
- Fix capabilities check in subscription decryption: use method existence
  instead of dynamic capabilities getter that returns false during lazy
  extension injection
- Wrap Promise.any decryptor calls in Promise.resolve().then() to catch
  synchronous throws from NIP-07 adapter methods when extension modules
  haven't loaded yet
- Make watch-history feed registration race-safe with pre-check and
  try/catch for concurrent registration attempts
- Improve NIP-07 extension detection with DOMContentLoaded listener for
  faster detection of extensions that inject during page load

https://claude.ai/code/session_0174XTB5ADJ9cMBSucqL9Wer
2026-02-09 00:18:43 +00:00
thePR0M3TH3AN a54e3c1329 Merge pull request #2257 from PR0M3TH3AN/claude/fix-logout-button-fxhy2
Deduplicate concurrent auth logout calls
2026-02-08 14:31:38 -05:00
thePR0M3TH3AN 7e07e1ce40 Unify signer-readiness gating for login list sync 2026-02-08 14:27:35 -05:00
thePR0M3TH3AN fd3f9e723a Split subscriptions/hashtag readiness and expose unavailable preference states 2026-02-08 14:10:39 -05:00
Claude 47fdad0ef2 Fix logout button not actually logging out the user
The auth:logout event listener in applicationBootstrap.js fires
synchronously during authService.logout(), setting __handled = true and
starting handleAuthLogout as fire-and-forget. When requestLogout() then
checked detail.__handled, it saw true and returned early — before
applyLoggedOutUiState() could update the UI.

Two changes fix this:

1. Remove the __handled early-return guard from requestLogout() so it
   always awaits handleAuthLogout.

2. Add a promise-based dedup guard to handleAuthLogout so the event
   listener's fire-and-forget call and requestLogout's awaited call
   share a single execution instead of running the logout cleanup twice.

https://claude.ai/code/session_01ArpiYU9bNdzme5xBHWnKuS
2026-02-08 19:09:50 +00:00
thePR0M3TH3AN ffc248ecd3 Track auth list sync outcomes and expose retryable auth status 2026-02-08 13:52:04 -05:00