The login modal now shows <bitlogin-auth> -- a portable Nostr identity
behind a login name and password -- as the default sign-in path,
styled with bitvid's own brand tokens (accent-strong, surfaces, radii).
The existing NIP-07 extension, nsec, and NIP-46 remote-signer options
still work exactly as before, just collapsed under a new "Other login
options" fold instead of a flat "Available methods" grid; nip07 no
longer carries the stale "Recommended" eyebrow now that BitLogin has
that role.
BitLogin isn't a single button-click-then-await-a-promise flow like
the other providers -- it's a whole multi-screen sign-in/create/recover
UI -- so it gets a permanent, statically-declared widget in
components/login-modal.html (js/ui/bitloginModalIntegration.js relocates
it, never destroys and recreates it, since removal tears down its crypto
Web Worker) instead of a grid button. It still goes through the exact
same session-establishment path as every other signer: a new "bitlogin"
provider (js/services/authProviders/bitlogin.js) lets AuthService
resolve and activate it via the normal requestLogin -> handleAuthLogin
path, while providersForModal excludes it from the grid so it isn't
rendered twice.
Two things this needed beyond the wiring itself:
- js/app.js is one line from its lint/file-size ceiling, so the only
change there is renaming an import binding (providers -> providersForModal),
a net-zero line change. The actual widget/event wiring lives in a new,
unconstrained module instead.
- The login modal's HTML (including the permanent widget element) loads
asynchronously after bootstrap runs, so wireBitloginLogin() waits via
MutationObserver rather than assuming a specific call order.
Vendored via scripts/build-bitlogin-widget.mjs (mirrors the existing
Bitcoin Connect/Blossom vendor steps): BitLogin isn't published to npm,
so the widget build is copied verbatim from the sibling ../bitlogin
checkout into committed vendor/bitlogin/, then lazy-imported (like
Bitcoin Connect) so its ~350KB never weighs on visitors who never open
the login modal.
Gated behind FEATURE_BITLOGIN (config/instance-config.js), default on
for this unstable-branch experiment per AGENTS.md §1 -- reset to false
before this reaches main until verified end-to-end against production
relays.
Verified: npm run lint (clean), the full auth/login test suite (43/43,
including new coverage for the provider + adapter), and a manual
Chrome pass against the built dist/ -- widget renders themed correctly,
"Other login options" still shows the original four methods unchanged.
Adds a second entry point (alongside the Safety-tab npub input) for the
per-author moderation override. On blurred/warned video surfaces, an
"Always show creator" button now sits beside "Show anyway". Clicking it
dispatches a `video:trust-author` document CustomEvent; the single app-level
listener in applicationBootstrap.js applies the account-level override and
refreshes affected videos, clearing the web-of-trust warning for every video
from that creator until reset.
Wired into VideoCard, VideoModal (persistent reused button + teardown
cleanup), and SimilarContentCard.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add "trusted creator" overrides that suppress the web-of-trust warning/blur
for ALL of a creator's videos until reset — the account-level companion to the
existing per-video "Show anyway".
- cache.js: per-author override store (bitvid:moderationAuthorOverrides:v1),
keyed by author hex, with get/set/has/clear/list + persist/load, mirroring
the per-video store.
- moderationDecorator: overrideActive now also honors an account-level override
for the video's author, so the whole class of WoT warnings clears for every
video by that creator.
- app.js: load the author overrides on startup.
- Services + contract: getAuthorModerationOverrides / setAuthorModerationOverride
/ clearAuthorModerationOverride; set/clear refresh the feed + fire the shared
moderation-override event so the Safety tab list updates.
- Safety tab: a "Trusted creators" section — add by npub, list each with a
Reset, mirroring the Active-overrides section.
- tests: storage set/get/has/clear/list, hex normalization, persist→load,
invalid-input guard.
Next: the "Always show this creator" button on the video warning overlay
(second entry point) — a larger multi-component UI change, done separately.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Resolved submissions were resurfacing (most visibly after a cache clear)
because the admin resolved-set wasn't durable: it was never mirrored (relays
pruned it) and the read-modify-write could republish a SHRUNK replaceable
list when a cold-load read missed the current set.
- Device-local resolved-id cache (localStorage): grows only, backs up the
relay list. Hides handled items even if a relay read momentarily misses,
and makes markSubmissionResolved loss-proof — it folds every locally-known
resolved id into each republish, so a missed read can't drop entries.
- Mirror the resolved-set on each fetch (re-broadcast) so relays keep it
alive, same as we do for pending submissions.
- Persist relay-known resolved ids into the local cache on fetch.
- Belt-and-suspenders: an application whose applicant is already whitelisted
is treated as handled even if the resolved-set were lost entirely
(callers now pass accessControl.getWhitelist()).
- tests: whitelist-hide, local-hide after a cold read, and loss-proof
republish (earlier id carried forward despite a missed read).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- adminBadge.js: add applyAdminRing(el, id) for surfaces that already place the
star on a specific element and just need the ring.
- comments: ring + star on admin authors' avatars; round the comment avatar box
so the ring reads as a circle.
- channel page: ring the inner rounded avatar box (star already on the wrap).
- Notification dots: red dot on the profile modal's Admin nav tab and on the
Submissions subtab button when there are pending submissions, so admins know
where to look. profileModalController.setSubmissionsUnreadIndicator toggles
both (Admin nav dot gated on the tab being visible); driven from
applyProfileNotificationDot off app._submissionsPending.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Make the super-admin/admin signal unmistakable on your own profile button:
a gold ring around the avatar (with a thin page-colored gap so it reads as
a ring, not a border) in addition to the corner star.
- applicationBootstrap.js: refreshAdminStar toggles an is-admin class on the
profile button using applyAdminStar's return value.
- CSS: #profileButton.is-admin > div draws the gold ring (box-shadow, not
clipped by the avatar's overflow).
- uiCoordinator.js: refresh the admin star/ring inside applyAuthenticatedUiState
so it also lights on session restore, not only a fresh login — the likely
reason it wasn't showing.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
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]>
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]>
New-user onboarding per docs/onboarding-plan.md: on the first login of a pubkey
on a device, a skippable guided tour escorts the user around bitvid — a scrim
dims the page with a spotlight CUTOUT over each step's anchor, ringed by a
pulsing glow driven by the theme's accent tokens, plus an anchored popover with
Back/Next/Skip, progress dots, and arrow/Esc keyboard nav.
- js/ui/onboarding/tourEngine.js: dependency-free engine. Spotlight = one
element whose 200vmax box-shadow (--surface-overlay-darker) dims everything
except the cutout; an ::after ring animates between --color-accent and
--color-accent-strong. Positioning via CSS custom properties set from JS (the
sanctioned dynamic-var pattern; visual styling lives in an injected token-only
stylesheet). Steps with missing anchors auto-skip, so one script serves all
layouts. Repositions on resize/scroll.
- js/ui/onboarding/bitvidTour.js: the step script (welcome → feeds →
subscriptions → upload → profile → setup CTAs that deep-link into the storage
and wallet panes via profileController.show).
- js/services/onboardingService.js: once-per-pubkey-per-device flags
(bitvid:onboarding:v1, settingsRestorePrompt-style).
- app.maybeOfferOnboardingTour fires 3.5s after login (feed already rendered
thanks to the warm cold-boot work; never blocks login);
app.startOnboardingTour({force}) + a "Take the tour" button in the profile
modal footer make it re-launchable.
- Lint allowlists: tourEngine.js added to the inline-style allowlist (CSS-var
positioning, VideoCard precedent) and the design-token ALLOWED_FILES
(injected stylesheet geometry, RevertModal precedent) — all colors are tokens.
Tests: tests/onboarding-tour.test.mjs (5 — flag lifecycle, tour render/advance/
finish, skip teardown, missing-anchor exclusion, once-vs-force offering, final
CTA deep-link + completion). Lint + build green.
Phases 2-4 (getting-started checklist, empty-state upgrades, fresh-npub create-
account + key backup + kind-0 bootstrap) tracked in the plan + TODO #59.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The opt-in "keep unlocked" cache had no user-facing way to undo it mid-session
(it cleared only on logout / clear-site-data). Add a "Lock this device" button to
the profile modal footer (desktop + mobile), shown only when the active account
has a cached key to forget (app.isSessionKeptUnlocked -> client.hasCachedUnlockedKey).
Clicking it runs app.lockKeptUnlockedSession(pubkey), which re-locks in one action
everything the signer unlocks:
- forgets the cached key (both tiers) so future reloads re-prompt for the PIN,
- drops the in-memory signer (logoutSigner) so the CURRENT session re-locks now
(the next signing action prompts again via ensureEncryptionCapableSigner),
- storageService.lock(pubkey), and
- nwcSettingsService.clearCache() (re-hydrates from the signer next time).
Visibility refreshes on renderSavedProfiles + selectPane. Tests: 2 in
tests/profile-modal-controller.test.mjs (shown only when kept-unlocked; click locks
the active pubkey). Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A persisted nsec session restores the pubkey + UI on reload but NOT the in-memory
signer (the private key is only passphrase-encrypted — TODO #36). So after a refresh,
adding/editing a hashtag on an nsec account dead-ended with "Connect a Nostr signer
that supports encryption before managing hashtag preferences" — even though the user
looks logged in. Same class blocks subscriptions/blocks/DMs/reactions.
New reusable gate app.ensureEncryptionCapableSigner({ pubkey, need, promptMessage }):
checks the active signer's capabilities; if it can't encrypt/sign AND a MATCHING
locked nsec key exists for the account, prompts once for the passphrase and re-unlocks
the signer app-wide via unlockStoredSessionActor. One unlock re-enables every signing
flow until the next reload. Never unlocks a different account's key (pubkey-mismatch
guard); "no-stored-key" leaves non-nsec cases (e.g. an unresponsive NIP-07 extension)
to the caller's own error. Branching lives in js/nostr/ensureSignerDecision.js (pure,
unit-testable without the app shell).
Wired hashtag add/edit through it (ProfileHashtagController.persistHashtagPreferences):
a cancelled prompt aborts silently, a bad passphrase shows its own toast, and publish
never runs until the signer is confirmed. Exposed to the modal as
services.ensureEncryptionCapableSigner.
This is the shared machinery for the whole reloaded-nsec cluster (#51/#54/#56/#57);
subscriptions/blocks/DM-send/reactions are mechanical fast-follows against the same
gate (tracked in TODO #57).
Tests: tests/ensure-signer-decision.test.mjs (9 — capability + recoverable/fail
branching, never-wrong-key), + 2 wiring tests (gate blocks publish until ok) in
tests/profile-modal-controller.test.mjs. Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
Follow-up to the locked-nsec detection: clicking "Unlock Storage" on a locked
persisted-nsec session (no in-memory signer after a page reload) now auto-opens the
login modal straight to its existing unlock-saved-key (passphrase) flow, so the user
re-unlocks in one step instead of hunting for the Login menu. Once unlocked, the
signer is active app-wide and re-opening the storage pane auto-unlocks.
- applicationBootstrap: add a side-effect-free openLoginModal service to
profileModalServices (app.loginModalController.openModal — no add-account callback;
idempotently ensures the modal is initialized first).
- ProfileStorageController: reportLockedNsecSession({ autoOpenLogin }) opens the modal
only from the user-initiated unlock click, never from the passive pane render (no
surprise popups).
- Tests extended to 8 (auto-open fires on click; stays closed on passive render).
Build + lint clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The video modal's ⋯ trigger declares its context via `data-more-dropdown="modal"`,
but attachMoreMenuHandlers read only `data-context`, so it defaulted to "card",
skipped the modal video resolution, and opened the popover with a null video. The
panel is built from that video, so every menu item got an empty data-event-id and
both Event Details and Popularity reported "No video selected."
- Read context from `data-more-dropdown` as well as `data-context`.
- Modal branch falls back to the modal's `activeVideo` when getCurrentVideo() is
null (deep-link opens don't set the playback service's currentVideo).
- Defense-in-depth: handleEventDetailsAction resolves the video from
app.videosMap.get(eventId); view-stats resolves via a new getVideoByEventId
callback instead of a risky `|| currentVideo` that could show the wrong stats.
- Regression tests in tests/more-menu-controller.test.mjs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
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]>
Live logs proved ingest works (subscribed to 17 whitelisted authors,
received 61 events, injected 39) — but injecting foreign authors during
cold-start triggers the moderation/WoT stage to hydrate their social graph
(kind 30000), which competes with the initial feed load and stalls it
("Fetching recent videos…" hanging, needs a refresh).
- startWhenFeedReady(): defer the first ingest subscription until the
native feed's first videos:updated (with a 12s fallback for empty
feeds), keeping ingest off the cold-start critical path. Wired in
applicationBootstrap (kept thin so the size cap holds).
- emitRefresh now logs how many ingested videos survive the feed filter
("N ingested of M in filtered feed") so we can tell whether foreign
videos are filtered out vs. injected-but-not-rendered.
Tests: defer-until-feed-ready + fallback-timer; mutation-verified.
1236/1237 unit (known UploadModal flake).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Pulls NIP-71 video events published by other Nostr apps (kinds
21/22/34235/34236), converts them via the ingest adapter, and injects
them into nostrClient.activeMap/allEvents so they flow through the
existing feed pipeline. Gated by FEATURE_NIP71_INGEST.
Mirrors bitvid's whitelist model: since the whitelist is "authors
allowed to publish/render here," ingest scopes its relay subscription to
whitelisted pubkeys while whitelist mode is on (bounded, no firehose) and
re-subscribes when the whitelist changes. When a deployer disables
whitelist mode, ingest opens to all authors (capped by a limit). Foreign
content is still gated by the same render-time filter
(shouldIncludeVideo -> accessControl.canAccess): whitelist, blacklist,
NSFW (content-warning -> isNsfw), private-skip, plus trust/mute/blur from
the feed engine. Native bitvid videos always win a shared root; among
ingested videos newest wins. Refreshes via the existing videos:updated
reactive path.
- accessControl.getWhitelistPubkeys(): hex whitelist for subscription scope.
- Wired in applicationBootstrap (start on init, stop on teardown).
- nostrService untouched (at its size cap); logic is a standalone service.
Mutation-verified: native-wins, whitelist scoping, feature gate.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Closes the dual-event drift the user flagged: an opted-in 34235 mirror now stays
in lockstep with the canonical 30078 automatically — no per-video babysitting.
- js/services/nip71MirrorSync.js: syncNip71MirrorAfterEdit (re-publish if still
eligible; unshare + clear flag if it became ineligible, e.g. flipped private),
syncNip71MirrorAfterDelete (remove + clear flag). Plus initNip71MirrorSync()
which subscribes to nostrService's EXISTING videos:edited/videos:deleted events
— so the size-capped nostrService is untouched.
- js/services/nip71MirrorFlags.js: pure resolveEditSync/resolveDeleteSync decisions.
- applicationBootstrap: one-line initNip71MirrorSync(app.nostrService) wiring.
- All best-effort: a mirror failure never breaks the core edit/delete.
tests: resolver decisions (lockstep/unshare/none) + delete clears the flag +
event wiring is idempotent. Mutation-verified (eligibility branch load-bearing).
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]>
- 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]>
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]>
The user's block/mute list (NIP-51) was not being checked when
processing direct messages, allowing blocked users' messages to appear
in conversations, trigger notifications, and increment unseen counts.
This adds block filtering at four layers:
- nostrService.applyDirectMessage: drops incoming DMs from blocked users
before they enter the in-memory message cache
- nostrService.persistDirectMessageRecord: prevents blocked DM senders
from being persisted to IndexedDB or triggering notifications
- profileModalController.buildDmConversationData: filters blocked users
from the conversation list UI (catches pre-block persisted messages)
- applicationBootstrap: excludes blocked-user conversations from the
unread DM badge count
The block checker is injected via setDmBlockChecker() to keep
nostrService decoupled from userBlocks, wired up in app.js init().
https://claude.ai/code/session_01EAVLtAM14GN99Eik8otLg2
- Moved `showError`, `showStatus`, `showSuccess`, and `updateNotificationPortalVisibility` logic to `js/ui/notificationController.js`.
- Instantiated `NotificationController` in `js/ui/applicationBootstrap.js`.
- Refactored `js/app.js` to delegate notification calls to the controller.
- Added unit tests for `NotificationController`.
- Followed AGENTS.md refactoring checklist.
Co-authored-by: PR0M3TH3AN <[email protected]>
Moves `handleAddProfile` logic from the main application class into `ProfileModalController` to improve separation of concerns as per `AGENTS.md`.
- Extracts `handleAddProfile` to `js/ui/profileModalController.js`.
- Updates `ProfileModalController` to handle add account flow internally.
- Removes legacy callback wiring from `js/app.js` and `js/ui/applicationBootstrap.js`.
- Adds unit tests for the extracted logic in `tests/ui/profileModalController-addProfile.test.mjs`.
Co-authored-by: PR0M3TH3AN <[email protected]>
This change ensures the 'Explore' link is visible in the sidebar when the user is logged in, and hidden when logged out, consistent with other authenticated routes like 'For You' and 'Subscriptions'.
- Updated `js/app.js` to handle `exploreLink` visibility in `applyAuthenticatedUiState` and `applyLoggedOutUiState`.
- Initialized `exploreLink` property in `js/ui/applicationBootstrap.js`.
- Updated `tests/app/hydrate-sidebar-navigation.test.mjs` to verify the behavior.