Identity pill top-center on every pane, straddling the header divider, so
users always see which profile they're acting as (e.g. before uploading).
Populated in renderSavedProfiles (same resolveMeta path as the Account pane
card, so it refreshes on profile switch), hidden when logged out, and
click-through jumps to the Account pane.
Co-Authored-By: Claude Fable 5 <[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]>
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.