The previous ZapActions.buildEventZapRequest signed a single zap request
to a single recipient. Notes carrying NIP-57 zap-split tags, NIP-53
live-activity host tags, or NIP-89 app-definition metadata expect the
payment to be distributed across multiple parties — so `amy zap event`
silently overpaid one party and underpaid the rest. The correctness
review on the action-set flagged this as the only real bug in the
extracted verbs; this commit fixes it.
* ZapSplitResolver — new commonMain object mirroring the resolution
order in ZapPaymentHandler.kt (splits > live-activity hosts > app
metadata > author fallback). Pure logic; pubkey→LN-address lookup
is passed in as a suspend lambda so amy reads from its file store
and Android reads from LocalCache, no shared cache-coupling.
* ZapActions.buildEventZapRequestsForSplits — high-level helper that
composes the resolver with per-share LnZapRequestEvent signing.
Each request's `relays` tag unions sender + author + recipient
inbox relays so the kind:9735 receipt routes to every interested
party (matches signAllZapRequests in the Android handler).
* amy zap event — rewired to the split-aware path. JSON output now
enumerates each recipient with its share, LN address, request id,
and BOLT11 invoice (or per-recipient invoice_error). Profile zaps
(amy zap user) keep the simple single-recipient path since they
have no split tags.
Tests: 12 new cases — LN-address splits, weighted pubkey splits, author
fallback, drop-silently-on-missing-LN, relay unioning, share rounding.
All 41 action tests green; both Android flavors compile.
Phase 2 of the iOS plan — clears the java.lang.ref.WeakReference
blocker from commons/commonMain. Four model files migrated; one
additional sync primitive replaced.
- Adds expect class WeakReference<T : Any> in
commons/commonMain/util/, with a jvmAndroid actual that typealiases
to java.lang.ref.WeakReference. iOS actual will typealias to
kotlin.native.ref.WeakReference when the target is added.
- Channel / Chatroom / MarmotGroupChatroom: the WeakReference(null)
initializer relied on platform-type nullability of
java.lang.ref.WeakReference's constructor. With T : Any in the expect
class, fields become nullable (WeakReference<...>? = null) and the
.get() callsites become ?.get(). Behaviorally equivalent.
- UserRelaysCache: same WeakReference migration, plus the
synchronized(this) double-checked-locking idiom is replaced with
co.touchlab.stately.concurrency.Lock + withLock (KMP).
kotlin.synchronized is JVM-only; Lock comes in transitively via
stately-concurrent-collections already added in the previous PR.
Model-layer @Synchronized usage in Channel/Chatroom/MarmotGroupChatroom/
Note (also JVM-only) is a separate iOS blocker and a separate PR.
Phase 2 of the iOS plan — clears the ConcurrentHashMap blockers from
commons/commonMain. Five files migrated (the four flagged in the
initial audit + ChessLobbyLogic, which used fully-qualified inline
java.util references that the import-based audit missed).
Adds co.touchlab:stately-concurrent-collections 2.1.0 — a small,
mature KMP library that provides ConcurrentMutableMap /
ConcurrentMutableSet with semantics equivalent to ConcurrentHashMap /
ConcurrentHashMap.newKeySet on every Kotlin target. The .block { }
helper covers the compound-update paths (ChessRelayFetchHelper's
per-relay event-count compute, ChessLobbyLogic's bounded-LRU dedup).
- ComposeSubscriptionManager + MutableComposeSubscriptionManager:
ConcurrentHashMap -> ConcurrentMutableMap
- ChessEventCollector + ChessEventCollectorManager: map and Set
- ChessRelayFetchHelper: in-function event/relay state
- ChessLobbyLogic: replaces dismissedGameIds (synchronizedSet),
recentlyLoadedGames (ConcurrentHashMap), seenEventIds (bounded LRU
using LinkedHashSet via Collections.synchronizedSet + synchronized {}).
seenEventIds keeps insertion-order eviction semantics because
mutableSetOf returns LinkedHashSet on every KMP target.
First Phase 2 verb wired through to the Android App Functions runtime so
Gemini (and other system agents) can drive Amethyst.
Scope is intentionally narrow:
* One read-only verb (searchProfiles), built on top of the existing
SearchActions in commons. No write verbs yet — they need a story
for NIP-46 / NIP-55 signer prompts from a background dispatcher.
* Play channel only. appfunctions 1.0.0-alpha09 is a Google AI alpha;
F-Droid builds continue to ship without any Google AI dependencies.
Architecture:
* AmethystAppFunctions — plain Kotlin host with @AppFunction methods.
The KSP-driven appfunctions-compiler discovers them and generates
the dispatch metadata XML at build time.
* PlayAmethyst — play-only Application subclass implementing
AppFunctionConfiguration.Provider; supplies the factory the
library uses to construct the host class. Manifest replaces
android:name in the play flavor only; F-Droid keeps the unmodified
Amethyst class.
* The androidx-provided PlatformAppFunctionService is registered in
the play manifest as the bind point — Amethyst doesn't ship a
custom Service.
KSP is now a project-wide plugin (apply false at the root); applied in
amethyst/ to run the appfunctions-compiler over the play sourceSet.
Amethyst becomes `open class` so PlayAmethyst can extend it. No other
behavior change.
Phase 2 of the iOS plan — two of the ~9 small migrations to clear
java.* imports out of commons/commonMain.
- ChessLobbyState: the AtomicLong stateVersionCounter only existed to
bump a MutableStateFlow<Long>. MutableStateFlow.update is itself
atomic, so the counter is redundant — replaced with
_stateVersion.update { it + 1 }. Removes the dep and simplifies the
code.
- SigningState (GlobalSigningStatus): AtomicInteger is doing real
cross-thread coordination. Migrated to kotlin.concurrent.atomics.
AtomicInt (KMP stdlib). The common-API method names differ from
AtomicInteger — addAndFetch(±1) / store(0) instead of
incrementAndGet / decrementAndGet / set.
Phase 2 of the iOS plan — first of ~9 small migrations to clear the
java.* imports out of commons/commonMain. Replaces java.util.Base64
with kotlin.io.encoding.Base64 (stdlib, KMP-clean). The two callers
(Android Base64Fetcher, Desktop DesktopBase64Fetcher) use the public
parse() signature only, which is unchanged.
Also documents the full Phase 2 audit in
amethyst/plans/2026-05-24-ios-support.md: out of 335 commonMain files,
21 are real iOS blockers grouped into ~10 small mergeable PRs. The
remaining 183 androidx.compose users and 7 androidx.lifecycle users
already map to JetBrains Compose Multiplatform / AndroidX KMP and need
no work.
Rewrites the policy/terms doc with three goals:
1) **Concise & easier to read.** Plain English, short sentences,
removed redundant intros (the "How Amethyst Works (and Why That
Matters Here)" block restated the Privacy intro), merged the
"Visibility" + "Permanence" sections into one paragraph, and
collapsed the Child Safety POC section into a single contact
block near the top of the document.
2) **More truthful.** Two corrections:
- F-Droid build uses UnifiedPush for notifications, not FCM. The
previous text only mentioned Google Firebase Cloud Messaging,
which was inaccurate for the F-Droid distribution.
- Replaced "We rely on Google Play's age verification to make sure
the user downloading the app is an adult" with "Amethyst's Google
Play listing is rated 17+. The app does not request or store age
information." Google Play does not actually verify user age, so
the old wording overstated the protection.
3) **Lower liability.** Several specific changes:
- Dropped the "We aim to acknowledge child-safety reports within
72 hours" service-level commitment that the solo developer cannot
reliably meet.
- Softened "we will recommend that the offending relay be removed"
and "What we can do: acknowledge the report, forward..." to
discretionary "may forward" / "may stop recommending" phrasing.
- Removed the absolute "data is strictly confidential and cannot
be accessed by other apps" guarantee. Replaced with the narrower,
verifiable claim that other apps cannot read app-local storage
on a standard, non-rooted Android device.
- Narrowed "Amethyst is built and distributed to comply with
applicable child safety laws and regulations" to "Amethyst is
distributed under Google Play's Child Safety Standards policy and
applicable law" — same in spirit, smaller surface for dispute.
Content that the Google Play Child Safety Standards checklist
requires is unchanged: explicit CSAE prohibition, child-safety point
of contact (amethyst@vitorpamplona.com), in-app feedback mechanism
(Report Post / Report Account / Block Post / Block Account / Block
Relay / Mute), method for addressing CSAM (in-app report → block
relay → NCMEC/INHOPE → optional developer notice), compliance
statement, and references to the app name "Amethyst" and the Google
Play publisher "Vitor Pamplona". The F-Droid carve-out also remains:
the MIT License in LICENSE is identified as the only instrument
governing source-built distributions, with no additional terms.
The Play-Store-only Terms-of-Use checkbox and the "About & Legal"
Settings section both contain hardcoded GitHub URLs to the published
PRIVACY.md. Previously they were in src/main and gated at call sites
by `BuildConfig.FLAVOR == "play"` — which works at runtime but still
compiles the GitHub URLs into the F-Droid APK.
This commit moves the URL-bearing UI into src/play and adds empty
src/fdroid stubs with the same signatures, so the F-Droid build is
physically free of the URLs.
New composables (same package + signature in both flavor source sets,
so callers in src/main link to whichever flavor is being built):
- com.vitorpamplona.amethyst.ui.screen.loggedOff.legal.TermsGate
* src/play: renders an AcceptTerms checkbox with the PRIVACY.md link
and the "acceptance required" error text.
* src/fdroid: empty body.
- com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.LegalSettingsSection
* src/play: renders the SettingsSection with Privacy Policy and
Child Safety Standards rows that open the GitHub-hosted PRIVACY.md.
* src/fdroid: empty body.
Call-site changes:
- LoginScreen: drop `BuildConfig.FLAVOR == "play"` guard and call
`TermsGate(...)` inside the existing `isFirstLogin` block. The
flavor source set picks the right body.
- SignUpScreen: drop `BuildConfig.FLAVOR == "play"` guard, call
`TermsGate(...)` unconditionally.
- AllSettingsScreen: drop `BuildConfig.FLAVOR == "play"` guard and
the inline About & Legal SettingsSection, call
`LegalSettingsSection()` instead. LocalUriHandler and BuildConfig
imports removed.
The old `src/main/.../loggedOff/AcceptTerms.kt` is deleted; its body
moves into the play-flavor TermsGate as a file-private helper.
The LoginViewModel / SignUpViewModel still use `BuildConfig.FLAVOR` to
decide the initial `acceptedTerms` value (so the login/signup button
isn't disabled on F-Droid). That check is logic only, contains no
URLs, and is safe for F-Droid distribution.
Verified: `grep -r "github.com/vitorpamplona/amethyst/blob"` against
src/main + src/fdroid returns zero matches. Both
:amethyst:compileFdroidDebugKotlin and :amethyst:compilePlayDebugKotlin
compile cleanly.
The one Jackson holdout in commons/commonMain. Migrating it unblocks
the iOS purity gate for :commons (Phase 1 of the iOS plan).
- Rewrites FeedDefinitionSerializer with the kotlinx.serialization JSON
tree API (JsonObject / JsonArray / JsonPrimitive). Wire format is
byte-identical, so users' existing on-disk custom-feed definitions
keep deserializing — covered by a new parsesLegacyJacksonOutput test
that pins a hand-written Jackson-shaped JSON blob.
- Adds :commons:verifyKmpPurity (mirrors the one in :quartz) and wires
it into the CI lint job alongside :quartz:verifyKmpPurity.
- Pulls in kotlinx-serialization-json as a commonMain dep; the
serialization plugin was already applied on :commons.
Third verb extraction alongside FollowActions / SearchActions, scoped
to event building so the action stays target-agnostic (commonMain,
no JVM/Android coupling).
* buildUserZapRequest / buildEventZapRequest wrap the two
LnZapRequestEvent.create overloads with a uniform call shape and
sensible defaults (PUBLIC zap, no LNURL, no poll).
* extractLnAddress pulls lud16 (preferred) or lud06 from a kind:0
metadata event, returning null when neither is set.
* satsToMillisats covers the sats→msats conversion that every
caller would otherwise duplicate.
Wires up amy zap user|event as the first consumer. The Lightning
round-trip (LNURL fetch + invoice retrieval) goes through the existing
LightningAddressResolver in commons/jvmAndroid; the BOLT11 invoice is
printed but not auto-paid since amy has no NWC wallet wired up yet.
F-Droid distributes Amethyst as MIT-licensed free software with no
acceptable-use terms layered on top — only the Play Store build needs
a ToS-acceptance checkbox at login/signup and links to the published
Child Safety Standards.
Gates added behind `BuildConfig.FLAVOR == "play"`:
- Settings → "About & Legal" section (Privacy Policy + Child Safety
Standards) is now Play-only; F-Droid settings no longer link to
PRIVACY.md.
- AcceptTerms checkbox in LoginScreen and SignUpScreen is now Play-only.
- LoginViewModel.load() / clear() and SignUpViewModel pre-accept
`acceptedTerms` on F-Droid so the login/signup button isn't disabled.
The Play build is unchanged: first-time login still requires checking
the ToS box, and the About & Legal section still surfaces the published
Child Safety Standards link required by Google Play.
Introduce SearchActions alongside FollowActions as the second of the
shared "verbs" usable by amy CLI and a future Android App Functions
adapter for Gemini.
* searchProfilesFilter / searchNotesFilter build the relay-side
Filter with the NIP-50 `search` field set; blank queries return
null so callers don't issue unconstrained searches that relays
would reject anyway.
* resolveSearchRelays picks the caller's kind:10007 list when
configured (decrypting NIP-44 private entries via the signer) and
falls back to DefaultSearchRelayList — the same set the Android UI
uses when the user has no list of their own.
Wires up amy search user|note as the first consumer.
F-Droid requires that apps add no restrictions to the FOSS license that
ships with the source. The previous wording ("Amethyst strictly prohibits
the use of the app to...") could be read as an EULA clause that
restricts use beyond what the MIT LICENSE grants.
Reframe the prohibition as a published community standard / acceptable-
use policy — which is exactly what Google Play's Child Safety Standards
policy requires anyway — and add an explicit "Free Software License"
note clarifying that the MIT license terms in LICENSE are unchanged,
and that F-Droid users and source redistributors retain every right
granted by MIT.
Google Play's requirements remain satisfied: the prohibition of CSAE is
still explicit, the in-app reporting mechanism is documented, the
child-safety point of contact and NCMEC escalation path are unchanged,
and the app/developer name is still referenced.
Phase 1 of the iOS support plan (amethyst/plans/2026-05-24-ios-support.md).
Two independent guards so JVM-only imports can't silently appear in
quartz's iOS-bound source sets:
- :quartz:verifyKmpPurity (Linux, ~1s): scans commonMain + apple/native
source sets for com.fasterxml.jackson / okhttp3 references and fails
the build with a clear pointer to the offending file:line. Wired into
the existing lint job so it runs on every PR.
- test-quartz-ios (macos-latest): runs :quartz:iosSimulatorArm64Test on
the simulator (NIP-04, NIP-17, NIP-19, NIP-49 vectors + AES-GCM and
chatroom-key tests already in quartz/src/iosTest) and additionally
compileTestKotlinIosArm64 to catch device-variant compile drift.
Google Play rejected v446 because the published Child Safety Standards
did not explicitly prohibit CSAE, name a child-safety point of contact,
describe the in-app reporting mechanism, or reference the app/developer
as listed on Play.
Rewrites the section in PRIVACY.md to:
- Explain that Amethyst is a client, not a host: third-party relays host
content and are responsible for moderation and any NCMEC reporting
obligations (e.g. 18 U.S.C. §2258A).
- Explicitly prohibit CSAE/CSAM in the app.
- Document the in-app tools users have: Report Post, Report Account,
Block Post/Account, Block Relay (NIP-51 Blocked Relay List), Mute
Words/Hashtags.
- Describe the escalation path: report in-app, block the hosting relay,
report to NCMEC CyberTipline / INHOPE, optionally email the developer.
- Provide a child-safety point of contact (amethyst@vitorpamplona.com)
with realistic scope of action (forward to relay ops, drop the relay
from default lists).
- Reference the app name "Amethyst" and developer "Vitor Pamplona" as
required by the checklist.
Also surfaces the document from inside the app by adding an
"About & Legal" section to Settings with two items: Privacy Policy and
Child Safety Standards (anchor link to the section).
Introduce commons/.../actions/FollowActions as the canonical, non-UI
entry point for NIP-02 kind:3 mutations. Accepts pubkeys as HexKey
rather than the Compose-bound User model, so callers without a cache
(amy CLI, future Android App Functions adapter for Gemini, automation
scripts) can drive follow/unfollow directly.
Kind3FollowListState.follow/unfollow now delegate to FollowActions,
preserving the existing Account.follow(user) signature on Android.
Behavior is unchanged for UI callers.
Wires up amy follow/unfollow as the first consumer — fetches the
freshest kind:3 from outbox relays before mutating so concurrent
follows from another client are preserved.
The previous attempt weighted every item, which made even Share collapse
to the left of its slice instead of pinning to the right edge.
Restore the natural-width carve-out for the last item, but gate it on
`!showCounter` — Share/Pay have no counter so they stay flush against
the right padding as before; Zap/Like/etc. become weighted when last so
the counter doesn't sprawl out to the edge and the row stays balanced.
LocalPreferences.setDefaultAccount called setCurrentAccount before
saveToEncryptedStorage. setCurrentAccount emits the new list onto the
savedAccounts MutableStateFlow, which AlwaysOnNotificationServiceManager
collects and reacts to by calling loadAccountConfigFromEncryptedStorage
for every saved account — including the just-added one. That call hit
encryptedPreferences(newNpub) before NOSTR_PUBKEY had been written, got
null, and cached the null in cachedAccounts.
cachedAccounts is a process-lifetime map, so the poisoned entry survived
the eventual disk write. Every subsequent switchUser to that account
took the cached null path, fell through to requestLoginUI(), and AccountScreen
rendered LoggedOffSetup — the onboarding screen with TOS unchecked, asking
the user to re-do the Amber handshake.
Write the per-npub file first, then seed the cache with the in-memory
AccountSettings, then publish onto the savedAccounts flow. Also stop
caching null returns in loadAccountConfigFromEncryptedStorage so any
future racy reader can't poison the cache either.
The reaction row gave every item except the last a `Modifier.weight()`,
which made the last item collapse to its natural width and hug the right
edge of the content area. With Share (icon-only) as the default last
item, all icons appeared evenly distributed.
When the user disabled Share, the last weighted slot moved to Zap. Zap
renders icon + counter, so its natural-width row took more space at the
right and pulled the rightmost icon away from where the other icons sat
(each at the left of a now-wider weighted slice), leaving the row
looking unbalanced.
Give every reaction an equal weighted slice so icons sit at the left of
their slice regardless of which reactions are enabled. The unused space
at the end of the last slice naturally provides the right-side padding
where Share used to sit.
Moves the asynchronous chain-verification side of NIP-BC onchain zaps out of
LocalCache into a dedicated OnchainZapResolver class living alongside other
NIP-specific subpackages under model/nipBCOnchainZaps/. LocalCache shrinks by
~240 lines and now owns only the synchronous event-dispatch responsibility:
loading the event, attaching the optimistic UNVERIFIED entry for the sender's
own zap, and delegating the verifier launch to the resolver.
The resolver owns:
- launchVerification(event, source, repliesTo) — async fire-and-forget
- reverifyOnchainZapsForNote(note) — used by the gallery's screen-driven loop
- onchainTipHeightFlow — shared chain-tip poller, lazy + WhileSubscribed
- verifyingEventIds / reverifyingNoteIds — in-flight de-duplication
- reverifySemaphore — parallelism cap
OnchainZapGallery now calls LocalCache.onchainZapResolver.{reverifyOnchainZaps
ForNote, onchainTipHeightFlow} directly. consume(OnchainZapEvent) passes the
already-computed repliesTo into launchVerification so the new-event path
doesn't recompute it on the verifier side.
No behavior change — all 22 onchain-zap tests still pass.
Test referenced formatSats, DEFAULT_ZAP_AMOUNTS, and ZapType which
were removed/made private in upstream merge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite SendDialog with sealed state machine that auto-detects input
type (BOLT11, LNURL bech32, lightning address). For LNURL/address:
resolves endpoint, shows amount form with min/max hint, optional
comment field, fetches invoice, then pays via NWC. Strips lightning:
URI prefix. Inline copiable errors with retry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SendDialog: switch to Dialog+Card with X close, inline copiable error
messages, button resets to "Pay Invoice" on error for retry.
LightningAddressResolver: return error body from callback responses so
server error messages (e.g. "Recipient wallet error") surface to user
instead of generic "Failed to fetch invoice". Also check "message"
field in addition to "reason" for error extraction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace AlertDialog with Dialog+Card pattern. Invoice created state now
shows centered amount, description, 240dp QR code, and full-width
"Copy Invoice" button. Close via top-right X button. Input form gets
full-width "Create Invoice" button.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add LaunchedEffect that rescans cache when followedUsers populates after
startup, fixing empty feed when contact list arrives after initial scan.
Remove diagnostic println from NwcPaymentHandler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LoginScreen's fire-and-forget save coroutine used rememberCoroutineScope
which got cancelled when the composable left composition after login.
Move saveCurrentAccount() to onLoginSuccess in Main.kt which uses the
app-level scope that survives recomposition. Fixes both nsec login and
generate-new-account flows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses the 15 issues from the second audit pass. Key changes:
- Per-event resolution flag (`Note.onchainZapResolved`) replaces the unbounded
rejection blocklist. The flag is set on terminal verifier verdicts
(Confirmed or hard-Rejected) and gates the verifier launch in `consume()`.
Travels with the Note so it clears on `removeAllChildNotes()`.
- Per-event in-flight set (`verifyingEventIds`) deduplicates concurrent
verifier launches across `consume()` echoes and `reverifyOnchainZapsForNote`
races. Solves: profile-only zaps bypassing the all-CONFIRMED guard,
Rejected entries re-firing the verifier on every echo, and the
consume()/reverify TOCTOU race.
- Per-note reverify gate (`reverifyingNoteIds`) prevents multiple visible
galleries from launching concurrent reverify passes for the same note.
- `removeOnchainZapForSource` now refuses to remove a CONFIRMED entry — only
an explicit fresh CONFIRMED replacement can change one. Prevents the
cross-target downgrade where one target's transient ZERO_VERIFIED_AMOUNT
erases a sibling target's already-confirmed entry. Also non-nullable
pubkey parameter to close the null-vs-null comparison hole.
- `innerAddOnchainZap` dedup tightened: exact structural equality skips
spurious flowSet invalidations on relay echoes, but same-level + equal
verifiedSats from a DIFFERENT source now replaces (fixes multi-signer
attribution lock-in).
- Tip flow uses explicit try/catch that re-throws CancellationException
instead of `runCatching` (same fix the previous audit applied to the
verifier). Lazy initializer falls back to a constant-null StateFlow if
`Amethyst.instance` isn't initialized yet, instead of throwing.
- Gallery driver: unconditional first-view kick (no longer waits for the
tip flow's first non-null emission), separate effect keyed on pending
entry count so a fresh UNVERIFIED arrival kicks reverify immediately
instead of waiting up to 60s for the next tip poll.
- `observeNoteZaps`'s memoization now keys on the `onchainZaps` map
reference so lightning-zap traffic on the same note doesn't churn the
onchain gallery.
- `reverifyOnchainZapsForNote` uses `supervisorScope` so a single failed
verifier doesn't cancel its siblings, and the semaphore permits bump
from 4 → 8 reduces head-of-line blocking when many galleries reverify
concurrently.
On resizing CDNs the imeta `x` (post-resize hash) can differ from the
`ox` (original hash) embedded in the URL. The bridge previously preferred
`explicitHash` over the URL's sha for "authoritative casing", but the
upstream file on `xs` is named after the URL's sha, not the imeta hash.
For URLs like https://image.nostr.build/<ox>.png with imeta x=<post-resize>
the cache would request /<x>.png and 404 on miss.
Always use the sha parsed from the URL path; drop the explicitHash
parameter. `extractSha256FromUrlPath` already lowercases, so the casing
concern is moot.
- Add kind 1 (replies) to interaction subscriptions
- Key count reads on FlowSet state for reactive updates
- Wire Quote menu item to ComposeNoteDialog with q-tag support
- Add BoostsPopup on long-press repost icon (who boosted)
- ComposeNoteDialog now accepts quoteOf param with nostr: URI pre-fill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>