Rewrites SetPasswordDialog.kt with modern 2024-2026 UX. Adds
"Privacy lock enabled" / "Password updated" confirmation snackbars.
Dialog changes:
- Dialog + Surface(shape=shapes.large, tonal=6.dp) shell instead of
default AlertDialog. Fixed width 440dp. Matches NewDmDialog.kt.
- Header row with Lock icon + title (titleLarge). Softer body copy
under the header for the first-time-set path.
- Set-a-password flow uses ONE password field with a reveal toggle
(Visibility / VisibilityOff, per-field independent). The reveal
toggle IS the confirmation — no more "confirm password" field.
Matches WhatsApp Chat Lock + macOS Users & Groups.
- Change-password flow uses two fields (current + new), each with
its own reveal toggle. Current is verification, not redundancy.
- Real-time checklist row under the New field: green CheckCircle +
"Min 6 characters" when satisfied, outlined Circle + muted text
otherwise. Copy-pattern from EditProfileScreen NIP-05 status.
- Save button disabled until the checklist passes.
- Bumps PRIVACY_LOCK_MIN_PASSWORD_LENGTH from 4 to 6.
- Auto-focus first field on open (LaunchedEffect + FocusRequester).
- Enter submits (via onPreviewKeyEvent + KeyboardActions.onDone).
- Escape / click-outside-dismiss are disabled to prevent accidental
loss of typed password (dismissOnClickOutside = false).
- Wrong-current error shown inline under the Current field.
- All reveal toggles reuse the KeyInputField.kt idiom verbatim.
Snackbar plumbing (scope-local — no CompositionLocal):
- PrivacyLockSettingsScreen owns a SnackbarHostState overlaid at
Alignment.BottomCenter. LockToggleCard receives an onSaved
callback, fires "Privacy lock enabled" on first-time set or
"Password updated" on change.
- DesktopMessagesScreen (banner path) owns its own SnackbarHostState
overlaid at BottomCenter. MessagesFirstRunBanner takes an
optional onSaved callback (default {}), fires "Privacy lock
enabled" after the dialog saves.
Adds an inline banner at the top of the Desktop Messages deck column
that nudges users to enable the privacy lock. Fires only when
!lockEnabled && !firstRunCardSeen; dismissal is sticky across
restarts + lock enable/disable cycles.
- MessagesFirstRunBanner: AnimatedVisibility(expandVertically + fadeIn)
wrapper around a Surface + Row with a padlock icon, title, body,
and Enable / Not now buttons. Modeled on OfflineBanner.kt.
- SetPasswordDialog extracted from PrivacyLockSettingsScreen.kt into
a shared desktop/security/ file so the banner and the settings pane
both point at the same composable.
- MessagesLockState.onUnlockSuccess() relaxed to accept Disabled as a
valid previous state, so enabling from the banner keeps the user
Unlocked and doesn't flash the lock screen. New unit test covers
this path; all 9 tests green.
- DesktopMessagesScreen wraps its two-pane / compact layout in a
Column with the banner on top and a Box(weight(1f)) around the
panes so fillMaxSize propagates correctly.
Phase 5 (Desktop-only). Wraps the Messages deck column behind a
PBKDF2-hashed password gate; drops the Android-app slice.
- PrivacyLockSettings gains passwordHashed field + setter (salt$hash,
base64). Backed by java.util.prefs on desktop.
- PasswordHasher: PBKDF2-HmacSHA256, 100k iterations, 16-byte salt,
256-bit key, constant-time compare. Same primitive family as
SecureKeyStorage.
- DesktopMessagesLockGate: synchronous branch select in composition
(no LaunchedEffect guard) — closes the deep-link race per plan
§Security Hardening H1. Renders content when Disabled/Unlocked;
renders inline password TextField when Locked. Fires
MessagesLockState.onLeaveRoute() in DisposableEffect onDispose so
navigating away from the Messages column re-locks immediately.
- DesktopMessagesLockGate handles the "no password set" edge case
with a Disable-lock affordance.
- LocalPrivacyLockSettings CompositionLocal + LocalMessagesLockState
(from commons) both provided once at the App composition root in
Main.kt. Constructed with the existing windowScope so the state
holder's idle timer coroutines are lifecycle-scoped to the Window.
- DeckColumnContainer: DesktopMessagesScreen wrapped in
DesktopMessagesLockGate for the Messages column.
- Desktop PrivacyLockSettingsScreen: Column + Card layout matching
LocalRelaySettingsScreen (no Scaffold). Toggle, "Change password"
affordance with a full set/change dialog (old + new + confirm),
inactivity timer dropdown (1m / 5m / 15m / 1h / Never), redaction
level dropdown (Hidden / Full), honest limitations copy. Auto-opens
the set-password dialog if user toggles ON with no password set.
- Slotted into the existing Settings pane in Main.kt right after
LocalRelaySettings.
Mine should download the user's own events from their outbox (NIP-65 write),
private-storage, local and proxy relays. The previous filterXMine path used
account.outboxRelays, which also included broadcast relays — write-only blast
targets that don't serve reads — so it queried the wrong set.
Add AccountMineRelayState (a sibling of AccountOutboxRelayState with broadcast
swapped for proxy) and feed it into the shared TopFilter.Mine resolver.
MineFeedFlow now pins authors=[me] to that fixed relay union via
AuthorsByProxyTopNavFilter — no per-author outbox resolution needed, since the
only author is the user and their relays are known from account state. Both the
DAL (liveXFollowLists) and the relay sub-assemblers (liveXFollowListsPerRelay)
consume it, and the per-relay flow re-emits when the mine relay set changes.
Add MineFeedFlowTest covering author scoping, the empty-relay case, and
re-emission on relay-set change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWUwciqZiXTkbYVSyiF2P4
The shared FeedTopNavFilterState.loadFlowsFor() mapped TopFilter.Mine to
AllFollowsFeedFlow, so "Mine" silently fell back to all-follows. Every screen
that offered the Mine chip (badges, communities, music tracks/playlists, git
repositories, nApplets, nSites, and the browser app rows) had to special-case
TopFilter.Mine on both the relay sub-assembler and the local DAL / display
filter to scope content to the user.
Introduce MineFeedFlow, which mirrors AllFollowsFeedFlow's outbox/proxy split
but pins the author set to the logged-in user's own pubkey. Because both
liveXFollowLists (DAL) and liveXFollowListsPerRelay (relay) derive from this
flow, the generic author paths now narrow to the user, so the per-screen Mine
branches are pure redundancy and are removed.
This also makes outbox-change invalidation automatic: the Mine relay set now
comes from liveXFollowListsPerRelay, an OutboxLoaderState over the user's own
outbox, so a NIP-65 update re-emits through the existing followsPerRelayFlow
collector without a screen-specific trigger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWUwciqZiXTkbYVSyiF2P4
Adds a repo-root cross-module roll-up that stitches the 10 per-folder
plans/ indexes into one view: totals, a per-module status table, and a
"live work" section listing every in-progress / queued / abandoned plan
with links. Shipped plans stay in each folder's archive/ and are linked
via the per-module README.
142 plans: 122 shipped, 9 in-progress, 8 queued, 3 abandoned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
Audited all 143 plan files across the 10 plans/ folders. Each plan now
carries a Status header (shipped | in-progress | queued | abandoned)
backed by codebase evidence, and every folder has a README.md index
grouping plans by status.
Shipped plans were moved into a per-folder plans/archive/ (via git mv,
history preserved) so each plans/ folder surfaces only live work:
shipped (archived): 122 in-progress: 8 queued: 7 abandoned: 4
docs/plans/ is the frozen legacy folder; its plans were stamped and
indexed in place (48 of 52 archived) but it remains closed to new plans.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
Adds a Follow Packs experience to Amethyst Desktop:
- New "Discover" sidebar destination with featured-pack hero, hashtag chips
driven by NIP-12 `t` tags, a 3-up "From the pack" notes feed, and a
right-rail of mini pack thumbnails.
- New "Follow Packs" launchable column (App Drawer + Discover "Browse all")
with multi-field search across title, description, creator name/npub,
and `t` tags.
- Pack detail overlay (read-only) with per-member Follow/Unfollow buttons
that reflect the live kind-3 state, plus pack-level Follow all /
Unfollow all with a dedupe-aware confirm dialog ("Follow N new (M
already followed)").
- Bulk follow / unfollow batched into a single kind-3 publish via new
`FollowActions.buildUnfollowBatch` and `Kind3FollowListState.follow/
unfollow(users: List<User>)`. The mutating call sites are Mutex-
protected against concurrent races.
- naddr → 39089 references in notes render as a rich inline card with
avatar stack + Follow all CTA. Cache miss triggers a one-shot
subscription; empty / deleted packs render minimal states.
- Shuffle button rotates both the featured pack and the gallery,
excluding the last 5 shown.
- Pack image fields render via Coil `AsyncImage` with a deterministic
gradient fallback.
Protocol additions:
- Quartz: `FollowListEvent.hashtags()` convenience accessor.
Bug fixes wrapped into the feature:
- `DesktopLocalCache.consumeContactList` now also loads the event into
`addressableNotes` so `Kind3FollowListState.getFollowListEvent()`
returns the user's actual kind-3. Without this, every bulk follow
silently replaced (rather than appended to) the contact list.
- Added Material Symbols `Shuffle` codepoint and regenerated the
bundled subset font (still 432 KB).
Add a direct link to each running app's editable Connected Apps
permission-detail screen from its top pull-down sheet, so users can
change the trust level and per-capability grants as they navigate.
Covers every top pull-down rendering:
- Embedded napplet/nsite and web-app tabs (Compose TopControlSheet),
keyed by the napplet `pubkey:dtag` coordinate or the web client's
`browser:<origin>`.
- Full-screen sandbox host and direct-browser activities (native
NappletControlSheet), via a new MSG_OPEN_PERMISSIONS IPC: the host
(which can't state its own coordinate) sends its launch token or
visited origin, and the main-process broker resolves the trusted
coordinate and opens MainActivity through a `connectedapp?coordinate=`
deep link added to uriToRoute.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dXhUL8To3qVXJVBZGCmhU
Refine the app-handler card rendering:
- NIP "Implements" bottom sheet now shows a wrapping row of clickable NIP
chips that open each spec in the browser, instead of a list of raw URLs.
- "Handles" gets the same "+N" overflow -> bottom sheet treatment as
"Implements", listing every handled kind.
- Related (`a`-tag) references now render as the referenced event's author
avatar + its real name with a short kind label, since these are vouched
for by the handler's author. Software Application shows the app name
(not the package-id d-tag) and is labelled "App"; Git repositories and
NIP-text events use their name/title, falling back to the d-tag until the
event loads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLfM7JUTr2vNiNUi4Ukmqn
Pressing Back on the "Connect to Nostr" first-connect dialog resolves to
AppConnectResult.Cancelled, which added the app's coordinate to an in-memory
`sessionCancelled` set. That set suppressed every future connect prompt for the
entire broker lifetime, so a later request — e.g. the user re-clicking "login"
in the in-app browser — was silently denied and the dialog never reappeared.
Because a Cancel persists nothing, the app also never showed up in Connected
Apps, leaving the user with nothing to clear to recover.
Replace the permanent suppression with a short, self-clearing cooldown
(`cancelledUntil` map): a Cancel suppresses re-prompts only briefly so the
burst of requests a page/napplet fires on load doesn't relaunch the dialog per
request, while a deliberate retry seconds later prompts again. The clock is
injectable for deterministic tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDcts4HzwSff4fy6oSVA6D
The full-screen direct-WebView browser (NappletBrowserActivity, launched when
you type an address) and the sandbox host (NappletHostActivity) used an older
console design that diverged from the embedded tabs' Compose chrome:
- The Console row in NappletControlSheet was a plain action row and the bottom
pull-up grabber was always visible. Make Console a Switch toggle (like the Tor
row / the Compose TopControlSheet), and hide the whole NappletConsolePanel
until the toggle is on — turning it on reveals the sheet already pulled up,
mirroring BottomConsoleSheet.
- The console grabber/panel sat at elevation 0 while the top sheet's panel is at
6dp, so an open top sheet drew over the console when they overlapped (e.g. in
landscape). Elevate the console sheet above the top sheet so its pull tab and
log render on top, matching the Compose layer where BottomConsoleSheet is
composed after TopControlSheet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9kfpNrBRB8WJNi67NHGGX
NIP-89 handler cards (kind 31990) previously only surfaced the supported
kinds and platform links, and even the platform links were silently dropped
when the link tag had no entity type. This widens both parsing and display:
- Fix PlatformLinkTag.match to accept 2-element link tags (entity type is
optional per NIP-89), so e.g. NostrHub's `["android", "intent:..."]` links
are no longer discarded.
- Parse the `i` supported-NIP tags (NostrHub points them at the NIP spec
markdown files) into a new SupportedNipTag, plus accessors for `t`
categories, `a` related addresses, and the `client` tag.
- Extend AppDefinitionEvent.build() with categories/supportedNips/
relatedAddresses/client so creation stays symmetric with parsing.
- Render the new data in RenderAppDefinition: category chips, compact
tappable rows for related addressable events (source repo, store listing,
...), a "via <client>" line, and NIP chips with a "+N" overflow that opens
a bottom sheet listing every supported NIP linking to its spec.
The deprecated `alt` tag is intentionally not surfaced on the handler card.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLfM7JUTr2vNiNUi4Ukmqn
Now that observeEvents re-emits the whole matching list each time,
GitStatusIndex and GitPullRequestUpdateIndex no longer need the imperative
launch/collect-into-MutableStateFlow wrapper carried over from the old
newEventBundles version. Each is now a single observeEvents().map { reduce }
.stateIn(scope, Eagerly, null) — dropping startIfNeeded(), the AtomicBoolean
double-start guard, and the MutableStateFlow/asStateFlow pair.
Eagerly (not WhileSubscribed) is required: callers read .value synchronously
(isClosedOrResolved, the feed filters, the home open-count derivations) and
must not see a stale map when nobody is collecting. All startIfNeeded() call
sites removed; stateIn shares one upstream subscription across collectors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
Replace the third "Repositories" tab on the default bookmark screen with a
dedicated entry on the bookmark-lists screen, mirroring how Pinned Notes
works: a row in ListOfBookmarkGroupsFeedView that opens its own
BookmarkedRepositoriesScreen via the new Route.BookmarkedRepositories.
The row shows the bookmarked-repo count from
gitRepositoryListState.publicRepositoryAddressSet; the screen renders the
BookmarkRepositoriesFeedViewModel feed (moved to a repositories/ package),
invalidates on bookmark changes, and preloads any uncached repo
announcements via the EventFinder. Reverts the tab added to
BookmarkListScreen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- GitStatusIndex now subscribes to a kind-1630..1633-filtered
LocalCache.observeEvents instead of LocalCache.live.newEventBundles,
matching the GitPullRequestUpdateIndex change: the indexed observable
seeds from the cache index and re-emits the full list on each new status
event, so the manual onStart full-cache scan and per-bundle type
filtering are gone and the collector just reduces to latest-per-target.
- Bookmark screen gains a third "Repositories" tab listing the user's
bookmarked (NIP-51 kind 10018) git repositories. New
BookmarkRepositoriesFeedFilter resolves the public repository address set
to addressable notes (newest first); the screen invalidates it on
publicRepositoryAddressSet changes and preloads any uncached repo
announcements via the EventFinder.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
GitPullRequestUpdateIndex now subscribes to a kind-1619-filtered
LocalCache.observeEvents instead of LocalCache.live.newEventBundles. The
indexed observable seeds its matching set from the cache index (via init())
and re-emits the full list on each new PR update, so the manual onStart
full-cache scan and the per-bundle type filtering over every event of every
kind are both gone. The collector just reduces the list to the
latest-per-parent map. PR updates are rare, so recomputing the whole map per
emission is cheaper than scanning every bundle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Top-bar repo bookmark toggle now switches between BookmarkAdd (with +)
and Bookmark glyphs instead of two identical glyphs, so the icon
visibly changes shape (not just tint) when starred/unstarred.
- Add a thin HorizontalDivider after the ReactionsRow on the repo home.
- Code browser: opening/closing a file or changing folders swaps the
scrollable in place, landing the new view at the top with no scroll
delta, which left the disappearing top bar stranded at its hidden
offset over a blank band. Expose the scaffold bar state via
LocalDisappearingBarState and reset it to visible on each in-place view
change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
The commonMain GitRepositoryListState used Dispatchers.IO without importing
the multiplatform kotlinx.coroutines.IO extension, so it resolved to the
JVM-only member and broke the iOS native compile
(:commons:compileKotlinIosSimulatorArm64). Matches BookmarkListState.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Converts the New Issue composer from an AlertDialog to a dedicated screen
(new Route.GitRepositoryNewIssue + GitNewIssueScreen with its own top bar
and a Create action). The Issues FAB now navigates to it.
- The extended FAB rendered square because the app theme sets shapes.large
(the extended-FAB default shape) to 0.dp; pin an explicit RoundedCornerShape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Repos that can't be cloned over http(s) (e.g. Iris's htree://) now show a
"Hosted externally" notice with an open-in-browser link instead of an empty
dashboard — on the home, the Code screen, and the feed repo card.
- Removed the "Maintained by" row from the project home.
- Tighter home section spacing (12 → 8dp) and a smaller bottom padding on the
feed repo card so the last-commit line sits closer to the reaction row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Snapshot cache: a process-wide GitRepoSnapshotCache keyed by repo address.
The browser ViewModel serves an already-fetched default-branch snapshot
synchronously, so the stats render in share-to-image and don't re-fetch when
switching screens.
- Issues/PR screens: filter chips now live inside the disappearing top bar
(via the scaffold's belowBar slot) so they hide with it instead of leaving a
static black band; the feed uses normal content padding.
- New issue is now an extended FAB on the Issues screen.
- Top bar shows the repo description as a single-line subtitle under the name;
removed the duplicate description from the home body.
- Tighter spacing: home sections, code header rows (branch row / search /
breadcrumb).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
The find-or-create wizard can surface a wallet the user previously
DELETED — a relay that missed the kind:5 still serves the kind:17375 to
the crawl. adoptDiscoveredWallet rebroadcast that event verbatim (same id,
same created_at), which loses to the prior NIP-09 deletion two ways:
DeletionEvent.build emits both an `e` tag (old id) and an `a` tag (the
replaceable 17375:pubkey: address), so relays reject the duplicate id and
re-delete every version with created_at <= the deletion's the moment the
kind:5 propagates back — on relays and in our own LocalCache. The
"reactivated" wallet would then silently vanish.
Adopt now re-signs a FRESH kind:17375 + kind:10019 (via publishWalletEvents)
with the discovered wallet's own mints and P2PK key. A new id escapes the
`e`-tag delete and created_at=now escapes the `a`-tag delete, while the
same key + mints preserve the nutzap address and all recoverable funds
(the NUT-13 seed derives from the key, not the event id). Falls back to a
verbatim rebroadcast only if the wallet can't be decrypted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
Deleting the Cashu wallet popped back to CashuWalletScreen, which on an
empty wallet auto-launches the find-or-create wizard — so the user was
funneled straight back into creating the wallet they just deleted.
Navigate to the top-level Wallet hub (Route.Wallet) via newStack instead,
which pops the Cashu screens off the back stack so the wizard never
composes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
In the find-or-create wizard, choosing "Create a new wallet" jumped
straight to the mint manager. Add a short celebratory interstitial first:
an animated check badge springs in (bouncy overshoot) behind an expanding
pulse ring, "Wallet Created" + "Now pick a few mints to host your sats"
fade up, a haptic fires, and a "Pick mints" button continues to the mint
selection.
The screen is purely presentational — the kind:17375 still isn't
published until the user adds a mint on the next screen — so "Pick mints"
uses popUpTo to replace the interstitial in the back stack, avoiding an
awkward return to the celebration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
- Recent-activity rows navigate to the issue/PR they represent (routeFor).
- The last-commit strip is now tappable: on the home it opens the Code
screen; on the feed repo card it opens the repository.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
Home screen:
- Nav cards: tighter vertical spacing; badges now count only OPEN issues/PRs,
derived from the live GitStatusIndex (started on the home so the split is
correct without visiting the Issues screen first).
- Moved the reaction row to after the recent-activity pulse.
- Added the standard 3-dot note menu (MoreOptionsButton) to the top bar.
Feed card (RenderGitRepositoryEvent):
- Replaced the web/clone links with the same stat tiles + language bar +
last-commit strip used on the home, loaded from a lazily-fetched shallow
snapshot. (Factory made internal so the card can build the browser VM.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Social bar now renders the app's canonical ReactionsRow (reply, boost,
like, zap, zapraiser, reaction gallery) instead of a bespoke subset, so
the repository announcement behaves exactly like any other note.
- Code browser file rows: replace the heavy 32dp boxed icon with a plain
20dp icon and tighten spacing/padding, reducing the oversized horizontal
gap before the file name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr