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]>
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]>
The admin pane stacked every list (Moderators / Whitelist / Blacklist) in one
scroll, and the per-event block list had no admin UI (block-only via the ⋯ menu,
no view/unblock). This reorganizes the pane into a sub-tab toolbar and adds the
missing Blocked-videos tool.
- Sub-tabs: a role="tablist" toolbar in #profilePaneAdmin switches between
Whitelist (default), Blacklist, Blocked videos, and Moderators. Moderators is
super-admin-only — its tab button is hidden otherwise. selectAdminSubtab()
shows one section at a time (ARIA + data-state), falling back to the default
when a hidden tab is requested.
- Blocked videos: view the per-event admin block list, unblock per row (confirm
→ removeFromEventBlacklist), and add by nevent/hex id. Rows show the video
title+author when it's in the local cache, else the shortened id. Reuses the
existing accessControl event-blacklist API + onEventBlacklistChange (live
re-render) and refreshes the grids on change. No backend changes.
- Row-building/render logic in js/ui/profileModal/blockedVideosSection.js
(keeps ProfileAdminController under its size cap; unit-testable without a DOM).
Spec correction: profile-modal-controller.test's admin assertion updated for the
sub-tab layout (Moderators section now hidden until its tab is active; its tab
button is shown to a super admin; selecting it reveals the section).
Tests: tests/profile-blocked-videos.test.mjs (7 — rows dedupe/enrich/fallback,
render, unblock wiring); profile-modal-controller admin test updated.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- `handleVisibility` only scheduled intervals but didn't trigger an immediate refresh when returning to the tab.
- This caused `tests/unit/services/exploreDataService.test.mjs` to fail (assertion `0 !== 1`).
- Added explicit calls to `refreshWatchHistoryTagCounts` and `refreshTagIdf` when `document.hidden` becomes false.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Moved controller instantiation (DM, Relay, Hashtag) to after services setup in `ProfileModalController.js`.
- Fixed `ProfileHashtagController` usage of uninitialized services.
- Updated `profile-modal-controller.test.mjs` mock to include `uiReady/dataReady` flags and use correct service access pattern.
- Resolved CI failures in `profile-modal-controller.test.mjs`.
Co-authored-by: PR0M3TH3AN <[email protected]>
Extracted ProfileDirectMessageController (split into Controller, Actions, Renderer, Helper) and ProfileRelayController from ProfileModalController to reduce file size and improve maintainability.
ProfileModalController.js size reduced from ~12000 lines to ~8750 lines.
Updated unit tests to reflect the new structure.
Co-authored-by: PR0M3TH3AN <[email protected]>
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]>
- Throttle the "Privacy warning: direct messages are using your default relays..." message so it only appears once per session/identity change.
- Rename "DM Relay Hints" to "NIP-17 Relay List" in the DM Settings modal to match the warning message and reduce user confusion.
- Add regression test for the warning throttling logic.
Co-authored-by: PR0M3TH3AN <[email protected]>
Added the missing `load()` method to the `hashtagPreferences` mock in `tests/profile-modal-controller.test.mjs`. This resolves the `TypeError` encountered during the `handleAddHashtagPreference` test case, ensuring the test suite passes consistently in CI.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Add missing `load` method to `baseHashtagPreferences` mock in `tests/profile-modal-controller.test.mjs` to fix `TypeError: this.hashtagPreferencesService.load is not a function` in unit tests.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Updates `tests/profile-modal-controller.test.mjs` to include a mock for `load()` in `baseHashtagPreferences`.
- Resolves the `TypeError: this.hashtagPreferencesService.load is not a function` error seen in CI.
- Ensures unit tests pass for ProfileModalController interactions with the hashtag pane.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Add `load: async () => {}` stub to `baseHashtagPreferences` in `tests/profile-modal-controller.test.mjs` to prevent `TypeError` during `handleAddHashtagPreference` test.
- Fixes CI failure in `tests/profile-modal-controller.test.mjs` where `this.hashtagPreferencesService.load` was undefined.
- Verified with `npm run test:unit:shard3`.
Co-authored-by: PR0M3TH3AN <[email protected]>
This change ensures that the profile history pane refreshes its content when the user navigates back to it, addressing a bug where stale data could be displayed.
It implements an optimized strategy:
- On the first load, `ensureInitialLoad` is called, which handles the initial fetch internally.
- On subsequent visits (when the renderer is already initialized), `refresh({ force: true })` is called explicitly to update the list.
The unit test `history pane lazily initializes the watch history renderer` was updated to reflect this behavior, verifying that `ensureInitialLoad` is called once and `refresh` is called on subsequent opens.