The browser/napplet strings (browser_address_hint, browser_console_title,
browser_console_title_short, browser_console_clear, napplet_untitled) were
moved to :commons, but their per-locale translations were left behind in
amethyst's values-*/strings.xml. With the default keys gone from amethyst,
lint flagged them as ExtraTranslation (80 errors across 16 locales).
Move the translations into commons/src/androidMain/res/values-*/strings.xml
so the default key and its translations live in the same module, preserving
the existing translation work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uza7sGxYPZtY43Ln2yH8FQ
Account deletion cleaned up the saved-accounts list and encrypted prefs but
never removed the on-disk files/accounts/<pubkey>/ directory (the MLS/Marmot
stores created in AccountCacheState.loadAccount). Every deleted or logged-out
account leaked its folder, so the on-disk account count drifted far above the
number of accounts shown in the switcher (16 dirs vs 6 saved on a test device).
- AccountCacheState: add deleteAccountFiles(pubkey) to remove the directory and
pruneOrphanAccountDirs(keepPubkeys) to clear dirs no longer backed by a saved
account.
- AccountSessionManager.logOff: delete the files in both delete branches.
- AppModules: one-time startup sweep, keyed by the hex of every saved account,
to clean up folders leaked before this fix.
- LocalPreferences.savedAccounts(): make the lazy init race-safe with a
dedicated mutex + double-checked locking. Multiple startup coroutines
(account load, always-on notification service, the new orphan sweep) call it
concurrently; the old check-then-act could run the IO read in parallel and
double-write ALL_ACCOUNT_INFO during the legacy migration.
Verified on-device: 16 -> 6 account dirs after one launch, stable across
restarts, no startup regressions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The versions collector resolved draftNote = account.getOrCreateDraftNote(...)
on every emission, including the content-less initial tick (~1s after the
composer opens). That touched the lateinit account before any user input; if
it were ever unset at that moment the throw would kill the collectLatest
coroutine and silently stop all draft saves for that composer.
Move the resolve inside the `if (it > 0)` guard, co-located with the save, so
account is only read once a real edit exists. The open-a-draft-then-send-
without-editing case is still covered by the explicit refresh in load().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
Three issues found in an adversarial audit of the wizard:
1. (correctness) recoverFromSeed bumped the NUT-13 counter only when
unspent proofs remained, but nextCounterAfterScan reflects every slot
the mint signed regardless of spent state. Adopting a fully-spent
wallet on a fresh device would leave the counter at 0, so the next
mint would reuse an already-signed slot and produce an unspendable
proof. Now bumps past every signed slot (matches the old
restoreFromMint behavior); the delta>0 check still no-ops when nothing
was signed.
2. (UX trap) the wallet screen's auto-launch-into-wizard guard used
`remember`, which resets when the screen leaves composition on
forward navigation — so backing out of the wizard re-fired the effect
and trapped the user in an inescapable loop. Switched to
rememberSaveable so the guard survives the round trip.
3. (robustness) wrapped the wizard's analyze() in try/catch so an
unexpected throw surfaces an error + retry instead of spinning on
"Analyzing…" forever.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
The screen is no longer an "add wallet" entry point — it's a settings-only
mint manager (first-time setup now goes through the find-or-create wizard).
Rename the composable AddCashuWalletScreen → CashuMintsScreen and the route
WalletAddCashu → CashuWalletMints to match its actual role.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
DraftTagState goes back to pure tag/version state — it no longer knows about
AddressableNote or needs an account-aware builder. Instead each composer
derives draftNote from the debounced versions collector it already runs:
on each emission it maps the current tag to its live cache note via
account.getOrCreateDraftNote(current). The ViewModel field holds the strong
reference that keeps LocalCache's weak entry alive until a deletion needs it.
load() refreshes draftNote after set(oldTag) because set() doesn't bump
versions, covering the open-a-draft-then-send-without-editing case.
deleteDraftInner takes the nullable note again and still only signs when the
note holds a real, non-deleted DraftWrapEvent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
Make the orbit design (centred gem ringed by three "server" nodes) the
always-on relay-service notification icon by writing it into the existing
amethyst_service drawable, so NotificationRelayService picks it up unchanged.
Remove the icon bake-off scaffolding now that a design is chosen:
- delete the alternative drawables (amethyst_service2..6)
- delete the DEBUG-only ServiceIconPreviewNotifications helper
- drop its trigger and now-unused imports from MainActivity
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
Grow the central Amethyst gem in each motif (orbit/sync/hub/waves) to the
largest size that still clears the surrounding elements. For the hub icon the
spokes now start farther from the centre to give the gem room.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
The Cashu "Add wallet" screen had become a mint manager, and its Create
path published a fresh kind:17375 that could clobber a portable NIP-60
wallet the user already owned in another client (kind:17375 is
replaceable).
When no wallet is loaded, drive the user into a new find-or-create wizard
that crawls every relay (modeled on the Event Sync tool) for the user's
existing kind:17375 wallets and branches on the result:
- 0 wallets: offer to create a new one.
- 1 wallet: verify + balance-probe it, adopt as the main wallet and
rebroadcast to outbox so it's easy to find next time.
- >1 wallets: the newest becomes the main wallet; older/duplicate wallets
are verified, their recoverable balances probed via NUT-09/NUT-07, and
the user gets one-tap "Recover funds to main wallet" per old wallet.
The mint manager (AddCashuWalletScreen) is now reachable only from Cashu
Wallet Settings, once a wallet exists; the no-wallet and "add wallet"
picker paths route through the wizard instead.
Implementation:
- Split CashuWalletOps.restoreFromMint into scanRecoverableProofs (no
publish, for the balance probe) + publishRecoveredProofs; foreign-seed
recovery never bumps the main wallet's NUT-13 counter.
- New CashuWalletDiscovery crawler reuses fetchAllPages + a fresh
NostrClient so crawled events don't pollute LocalCache.
- CashuWalletState gains decrypt/probe/recover/adopt helpers.
- Extract AccountViewModel's relay-crawl closures (crawlRelayDb,
buildCrawlClient) so Event Sync and the wizard share them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
Scale up the orbit/sync/hub/waves motifs and their centred gems so each
drawing occupies as much of the 512 viewport as possible (rings and nodes
pushed near the edge, larger central gem), keeping a small margin so strokes
don't clip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
Instead of capturing the AddressableNote returned by each save (and on
load) and re-assigning it, DraftTagState now builds the note from the
current tag via an injected (tag -> AddressableNote) builder and rebuilds
it whenever the tag changes. Because that note is the live cached object
for the address, its event tracks the draft automatically as it is saved
or removed, so:
- the note is never null inside the state (lateinit, wired by start());
- createAndSendDraftIgnoreErrors no longer needs to return the note, and
load no longer needs to capture it — set(oldTag) rebuilds it;
- the writer's existence check becomes "is there a real, non-deleted
draft event in the note" (DraftWrapEvent.isDeleted()), which also stops
a second blank-delete from re-signing an already-emptied draft.
ViewModels just wire draftTag.start(account::getOrCreateDraftNote) in
init() and reference draftTag.note; the per-VM field and held() are gone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
Add four additional always-on service notification icon candidates, each
keeping the centred brand gem with a "background service / connected to
servers" motif around it:
- amethyst_service3: orbit ring with three server nodes
- amethyst_service4: two looping sync arrows (running)
- amethyst_service5: hub & spoke to five nodes (connected to relays)
- amethyst_service6: concentric broadcast waves
To compare all candidates on a real device, add a DEBUG-only helper
(ServiceIconPreviewNotifications) that posts one always-on-style ongoing
notification per icon (same channel style, ongoing/silent/low priority),
triggered from MainActivity.onCreate under BuildConfig.DEBUG.
NOTE: the preview harness (ServiceIconPreviewNotifications + the
MainActivity hook) is temporary scaffolding for the icon bake-off and is
meant to be removed once a design is chosen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
The strong reference that keeps a saved draft alive (so LocalCache's weak
reference can't collect it before deletion) was duplicated as a draftNote
field across all eight composer ViewModels, each re-clearing it in
cancel(). The note's lifecycle is 1:1 with the draft tag, so DraftTagState
is its natural owner: it now holds the AddressableNote, exposes held() to
set it, and drops it in rotate() — which every cancel() already calls.
ViewModels now reference draftTag.note / draftTag.held(...) and no longer
carry their own field or reset logic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
The Add/Edit Cashu wallet screen no longer has a Save button. The NIP-60
wallet (kind:17375) and nutzap info (kind:10019) are now published the
instant the first mint is added, and re-published on every later add or
remove — so the list on screen always matches what's on relays.
This removes the confusing two-step flow where a typed/selected mint plus a
chosen key still left Save disabled until the user discovered the "+" button.
The explicit P2PK key picker (auto-generate / paste) is gone from this
screen: a nutzap key is generated automatically on first creation. Advanced
key import/rotation still lives in the settings Danger Zone.
Key safety: publishMints reuses the wallet's existing P2PK key on every
re-publish. The first publish's key is cached in the ViewModel and guarded
by a mutex, so rapid successive adds — firing before the new kind:17375
round-trips back through LocalCache — can't generate a second key and
rotate it, which would orphan inbound nutzaps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX5wkwXbvoQpadALx1ZVjk
LocalCache.addressables keeps AddressableNotes via WeakReference, so a
saved draft could be garbage-collected between creation and deletion.
The previous existence check (look the draft up by tag before signing)
would then find nothing locally and skip the deletion, leaving an orphan
draft on the relays.
Each composer ViewModel now holds a strong reference to its draft note:
createAndSendDraftIgnoreErrors returns the consumed AddressableNote, and
load()/editFromDraft captures the note when editing an existing draft.
deleteDraftInner takes that held note directly (sourcing the dTag and
relays from it) instead of a tag lookup, so it can always reach the draft
it needs to delete and still signs nothing when there is no draft.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
Keep the original hollow-outline amethyst_service icon and add the new
circular badge (solid disc with the gem punched out of its centre) as a
separate amethyst_service2 drawable, so the always-on notification icon
can be switched between the two without losing either design.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
The Crowdin-synced values-fr-rCA/strings.xml had unescaped apostrophes inside the
CDATA sections of block_hide_user and report_dialog_block_hide_user_btn, which
aapt2 rejects ("Invalid unicode escape sequence"), breaking resource compilation.
Escape them as \' to match the known-good values-fr translations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4rrFiWApq8EFpk4fSuTFH
The lightning, nutzap, and onchain activity cards embed the zapped post via
RenderZappedPost with makeItShort = true. The 2-line compact preview, however,
only triggered when the logged-in user authored the post
(makeItShort && isLoggedUser(author)). When the user is merely a zap-split
beneficiary of someone else's post, that check failed and the post rendered in
full instead of the intended compact preview.
Gate the short preview on the boosted-note flag as well, so any post embedded in
one of these activity cards (the only makeItShort callers that pass
isBoostedNote = true) is always shown as a 2-line preview, regardless of author.
Other makeItShort callers (Report, community post approval, attestation, reply
composition, compose screens) all use isQuotedNote instead, so their behavior is
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4rrFiWApq8EFpk4fSuTFH
The embedded in-app browser and napplet/nSite surfaces rendered web content in
the device theme, ignoring the app's DARK/LIGHT preference — a site with dark
support stayed light when the app was dark (and vice versa). The full-screen
activities had the same latent gap (they followed the device, not the app).
Root cause: WebView's dark decision (prefers-color-scheme via algorithmic
darkening) reads the context's THEME (?android:attr/isLightTheme), not just the
Configuration uiMode. The off-window SurfaceControlViewHost surface context
carries neither the host window's theme nor its night mode, so the renderer came
up light. The old applyNightMode() used UiModeManager.setNightMode — a
permission-gated no-op — so the theme never reached the WebView at all.
Fix: build every embed/host WebView from nightThemedContext() — a
ContextThemeWrapper over a forced-night/day Configuration with a DayNight theme,
so the theme's isLightTheme resolves from the app's resolved theme. Shared in
EmbedWebViewTheme.kt; used by NappletBrowserService, NappletHostService,
NappletBrowserActivity, and NappletHostActivity. Removed the dead applyNightMode
no-op from all four. (Verified on device with a throwaway SurfaceControlViewHost
repro: config-only context does NOT work; setForceDark is a no-op at targetSdk
37; setApplicationNightMode does nothing; the DayNight ContextThemeWrapper is
what flips the renderer, even across the cross-process embedded surface.)
Device-verified: all four surfaces follow the app theme even when it differs
from the device.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NIP-34 pull requests (kind 1618) and pull-request updates (kind 1619)
previously had no renderer and fell through to the plain text-note
branch, so a PR notification opened onto an unstyled markdown blob with
none of its structured data shown. Add dedicated cards that reuse the
existing patch/issue card vocabulary (bordered container, type chip,
status pill, embedded repository header) and surface the PR-specific
metadata the event carries:
- Pull Request card: "Pull Request" chip with a merge glyph, status
pill, subject title, branch name, current commit, merge base, and
clone-URL download rows.
- PR Update card: "PR Update" chip, repository header, new commit /
merge base, clone URLs, and an explanatory line (updates carry no
body content).
While here, modernize the existing cards consistently:
- Factor the shared markdown body, metadata row, and subject title into
reusable composables (GitMarkdownBody / GitMetaRow / GitSubjectTitle).
- Show the issue subject as a proper title. The old code cast the event
to TextNoteEvent to read the subject, which always returned null
(GitIssueEvent is not a TextNoteEvent), so issue subjects were never
displayed; read it from GitIssueEvent.subject() instead.
- Render the patch commit as an iconed metadata row.
Wire the two new kinds into NoteCompose and the thread detail view, add
the CallMerge / Commit / AltRoute Material symbols (font subset
regenerated), and add the new string resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCXL6btUeZZmSP1TCYkKh
deleteDraftInner() unconditionally signed a deleted-draft wrap event plus
a deletion event, even when no draft for that tag existed. With an
external NIP-55/NIP-46 signer, clearing a composer to blank could then
prompt for a signature even when "Automatically create drafts" is off,
since the blank-text branch of sendDraftSync() always calls delete.
Skip both signatures when the draft does not exist in the cache. The
existence lookup reuses the addressable note already fetched for relay
hints, so it adds no extra work, and real drafts are still deleted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
Two French-Canadian CDATA string resources carried a raw apostrophe
(`l'utilisateur`), which aapt2 rejects with "Invalid unicode escape
sequence", breaking every Android resource merge (merge*Resources) and
therefore the whole Android build. Escape them as `\'` like the other
apostrophes in the file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCXL6btUeZZmSP1TCYkKh
New pull request events (GitPullRequestEvent, kind 1618) and pull
request updates (GitPullRequestUpdateEvent, kind 1619) were never wired
into the notification pipeline, which only handled patches (1617) and
issues (1621). As a result repo maintainers received no notification
when a PR was opened against their repository, even though the PR event
p-tags the repo owner.
Add both kinds in all four places that gate notifications:
- FilterNotificationsToPubkey: request the kinds from relays so the
notification subscription actually pulls PR events.
- NotificationFeedFilter.NOTIFICATION_KINDS: let cached PR events through
to the in-app Notifications tab.
- NotificationFeedFilter.tagsAnEventByUser: treat PR/PR-update as
notifiable (mirrors the existing patch/issue handling).
- EventNotificationConsumer: dispatch PR/PR-update via notifyMention so
push notifications fire.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCXL6btUeZZmSP1TCYkKh
CommentPostViewModel.sendDraftSync() unconditionally signed and
published a draft event whenever the user typed in a comment, ignoring
the "Automatically create drafts" setting. Every other composer
ViewModel guards this path with
accountViewModel.settings.automaticallyCreateDrafts(); add the same
check here so disabling the setting stops generic draft events from
being signed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
Opening an embedded page full-screen and returning left every embedded WebView
in the :napplet process broken: dead DNS (ERR_NAME_NOT_RESOLVED), DOM reads
returning empty (a field that visibly shows text reports value==""), dead
text-selection highlight, and broken IME (caret stuck at 0, can't delete).
Two process-global defects in the full-screen hosts were corrupting the shared
WebView state the embedded surfaces rely on:
- pauseTimers()/resumeTimers() are PROCESS-GLOBAL — they pause/resume JS,
layout and parsing timers for every WebView in the process. The full-screen
activities (and the napplet embed pause/resume path) called them on their own
lifecycle, so returning from full-screen froze the embedded surfaces, which
have no resume of their own. Replaced with per-WebView onPause()/onResume()
(which pause only that surface's JS/DOM — still satisfies the napplet
background-security goal). No process-global timer calls remain.
- WebView.destroy() was called while the WebView was still attached to the
window, which corrupts the shared multiprocess renderer. Detach (removeView)
and stopLoading() before destroy() in both full-screen activities.
This also resolves the long-standing "selection highlight dead after a
full-screen excursion" blocker — same root cause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes parity #5. `fieldExtend` kept a pure word-snap, so the in-field
start/end handles couldn't be fine-tuned to a single character. Now it keeps
per-drag state (reset on a >250ms gap or edge switch) and matches native
`Editor` word-selection drags: the gesture baselines at the current selection
edge, sweeping past that word's far boundary snaps to the next whole word
(never stopping mid-gap), and moving within / back from the furthest-reached
word gives character precision. Symmetric for both handles. Page-text extend
stays character-granular.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds out host-drawn text selection for embedded napplet/nsite/browser
surfaces toward native parity, and fixes the bugs found while exercising it.
- Magnifier loupe (#4): EmbeddedMagnifier + provider-side pixel capture
(EmbeddedMagnifierProbe) shipped over IPC for both embed paths; the
caret/selection handles drive it via OnMagnify.
- SelectionUiState: single source of truth for the overlay show/hide rules
(insertion caret / in-field range / page-text range + dragging/scrolling).
- EmbeddedSelectionDrag: suspends the nav drawer's edge swipe while a handle
is dragged (auto-scroll #9).
Bug fixes:
- No more overlay blink on word-select: the shim's selection-reveal scrolls
(a textarea auto-scrolling to show a forming/re-asserted range) no longer
trip the hide-on-scroll path, and the hide self-heals instead of being
re-armed indefinitely.
- RemoteImeView debounces the range-lost signal so a transient collapse that
gets re-asserted doesn't flicker the handles/toolbar.
- Focusing a field clears any page-text selection (shim + host), so the stale
page handles/Copy bar no longer linger above — and stop stealing drags from —
the field overlays; also cancels any in-flight scroll-hide on focus.
- Caret insertion-handle drag now actually moves the caret: read the pointer
delta with positionChangeIgnoreConsumed() before consuming, so the value
isn't zeroed by our own consume (or the sandbox surface consuming the move).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tools/ime-test/index.html is a single-page input+textarea harness with an
on-page log timestamping focus/selection/input/composition events, paint
latency, long-tasks, and main-thread blocks — the instrumentation used to pin
the erase / caret-jump / first-letter-freeze bugs and what we'll use to profile
the magnifier. README documents serving it (python http.server on 8765,
10.0.2.2 for emulator / adb reverse for USB) and opening it as an embedded tab
via the in-app browser address bar. Plan gets a matching "How to test" section.
Dev tool only — nothing under tools/ ships, so the [ImeDiag] strings stay out of src/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inventory native Android text-field features (insertion/selection handles,
floating toolbar, magnifier, smart selection, etc.) with activation rules and
our current coverage; prioritize the magnifier and a centralized SelectionUiState.
Document the open full-screen round-trip bug that kills highlight paint across
all :napplet embeds, what was already ruled out, and the next hypotheses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Embedded WebView surfaces (:napplet process, SurfaceControlViewHost) can't
host the soft keyboard or present Chrome's own selection UI, so editing and
selection are relayed to the main process. This lands the working set of that
relay:
- shim.js: fix React-controlled input erase by writing through the native
HTMLInputElement/HTMLTextAreaElement value setter (so React's value tracker
stays in sync); make the host authoritative for selection re-assert (the
editable selectionchange handler only mirrors); report field/caret geometry
and page-text selection geometry; add pageExtend + caret coords (border-width
corrected) for drag-to-extend and the insertion handle.
- RemoteImeView: land caret where tapped on focus (requestFocus before applying
remote state); host-authoritative selection re-assert within a time window;
setText only when text actually changed; wire copy/cut/paste/select-all and
edit callbacks.
- EmbeddedTabLayer: stop resizing the surface on IME show (removes the ~1s
first-letter freeze); draw the selection overlay — toolbar, teardrop
selection handles, and the insertion (cursor) handle.
- EmbeddedImeBridge / Embedded{Napplet,Browser}Controller: carry selection +
caret geometry and the page-selection event across the Messenger channel.
Known limitation (not fixed here): after a field's page is opened in its own
full-screen activity and the user returns, the selection-highlight paint stays
off across all embedded surfaces. DOM selection, the toolbar, and copy still
work — only the native highlight is gone. This is a WebView/Chromium behavior
in off-window surfaces and is not reachable from the app layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The persistent relay-service notification previously reused a hollow gem
outline, which looked unpolished. Replace it with a full, solid disc that
has the brand gem punched out of its centre as negative space (slightly
smaller, with a comfortable surrounding ring).
The enclosing circle gives the always-running service its own distinct,
continuous "badge" feel while keeping the recognisable Amethyst gem shape.
Built from the real gem path so it stays on-brand; a single evenOdd path
turns the gem into a hole and restores its facet dot as a solid island.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
The global Apps feed (NIP-82 software apps) was sending a kinds-only REQ
filter with limit=200. zapstore's relay scores each REQ filter for
specificity and rejects anything that scores below 3, treating a kinds-only
filter with a limit >= 100 as "too vague" — so the whole subscription was
refused.
- Drop the global Apps filter limit from 200 to 99 so a kinds-only filter
clears the bar (kinds +1, limit < 100 +2 = 3).
- Always include wss://relay.zapstore.dev in the global Apps feed relays,
since it indexes the full app catalog, unless the user has added it to
their blocked relay list (NIP-51 kind 10006).
- Re-evaluate the subscription when the blocked relay list changes so
blocking/unblocking zapstore takes effect without an app restart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01US2aK3igzfeo4xtcvg3mKc
innerLoadCurrentAccountFromEncryptedStorage awaited ~27 parallel parse
Deferreds directly inside the single 70-argument AccountSettings(...)
constructor expression. Each await is a suspension point, so the coroutine
state machine had to spill and restore the entire partially-evaluated
operand stack (dozens of already-built MutableStateFlow args) at every one
of them. That inflated the generated invokeSuspend method past the JVM
per-method limit:
Method exceeds compiler instruction limit: 57866 in
LocalPreferences$innerLoadCurrentAccountFromEncryptedStorage$result$1.invokeSuspend
Resolve every Deferred into a local val first, then build AccountSettings
as a single straight-line, suspension-free expression. Each await now sits
at a statement boundary with a near-empty operand stack, so only a small,
linear set of locals is spilled. Parallel parsing behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6VdHHFRNYXKXFcW7j5sF7