mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
app 1.14.0 -> 1.15.0, appCode 457 -> 458. That single edit drives Android's versionName/versionCode, Desktop and CLI packageVersion, quartz's Maven version and geode's RelayInfo.VERSION. RELEASE_NOTES_ID is repointed at the v1.15.0 note (8fce45589ea44df75e828a04c7d70bb4fabedd6ffc1946a920b2f0c7c990ff9f), which RELEASE_OPS notes happens on x.y.0 releases and not on patches. It has to ship in this commit rather than after it: the drawer's "Release Notes" link and the donation card both open BuildConfig.RELEASE_NOTES_ID, so a tag cut before the repoint ships users a link to the previous release's note. Verified present on relay.damus.io and relay.primal.net before committing. Adds docs/changelog/v1.15.00.md, written from the 556 commits since v1.14.0, and its index entry. The cycle's headline is the Marmot resync: the MIP documents were deprecated in July and MDK followed, leaving our implementation invalid under either profile the current spec defines, so it moves onto the adopted current profile and is now interoperable with White Noise. Marmot group chat itself is not new -- it shipped in v1.09.0 -- and the notes say so. Also syncs the docs that state a version rather than illustrate one, since quartz and geode both read libs.versions.app: - README.md, quartz-integration SKILL.md and its gradle-setup.md reference -> quartz 1.15.0 - geode/README.md install commands -> geode 1.15.0 The Homebrew/Winget status blocks in BUILDING.md and RELEASE_OPS.md were re-verified rather than re-stamped, and the claim had gone stale in our favour: both Homebrew packages are live upstream now. formulae.brew.sh answers 200 for the amethyst-nostr cask (at 1.14.0) and for the amy formula, while geode-relay 404s and microsoft/winget-pkgs still has no VitorPamplona/Amethyst -- PR #422752 is open pending CLA. Both blocks now say that, RELEASE_OPS gains a geode-relay row, and the section heading no longer claims Homebrew is not shipping. Left alone deliberately: everything under */packaging/ and translators.json's tag, which the bump workflows and the Crowdin job write after the tag exists (bumping by hand would commit wrong hashes and a dead URL); and cli/tests/marmot/state/mdk/Cargo.lock, where 1.14.0 is an unrelated crate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VgVDQQXAg4cmzsWHoJj61k
340 lines
19 KiB
Markdown
340 lines
19 KiB
Markdown
# v1.15.0: Marmot Updates, a Advanced Search Box, and Books, DVM Updates
|
||
|
||
Highlights:
|
||
|
||
- Rebuilds **Marmot** onto the adopted spec — Amethyst groups now interoperate
|
||
with White Noise, in both directions.
|
||
- Makes the **search box's tokens real filters**, locally and on relays, and puts
|
||
Android and Desktop on one pipeline.
|
||
- Reads **publications, wikis, citations, library records and ratings**.
|
||
- Adds **NIP-A3 payment targets** to the zap picker, and stops NWC failing in
|
||
silence.
|
||
- Shows which **DVMs are alive** before you pick one.
|
||
- Brings **NIP-53 live streaming** to Desktop — discover, watch and chat.
|
||
- Stops fresh installs **stranding on a Tor bootstrap**.
|
||
|
||
## New Features
|
||
|
||
### Marmot Groups Catch Up With the Spec
|
||
|
||
Marmot deprecated the MIP documents in July and MDK followed, which left our
|
||
implementation — shipped in v1.09.0 — no longer a valid Marmot client under
|
||
either profile the current spec defines. This release moves it onto the adopted
|
||
"current profile" and proves it against the reference.
|
||
|
||
- Implements the MLS extensions draft's `app_data_dictionary`, the account
|
||
identity proof v2 that makes a group classifiable at all, and the six group
|
||
components that replace the single packed `marmot_group_data` blob.
|
||
- Replaces the old same-epoch tie-break — lowest `created_at`, then event id,
|
||
both of which a sender picks — with the spec's convergence: candidate branches
|
||
derived by replaying MLS bytes, a bounded pass, and a six-step comparison over
|
||
authenticated values only.
|
||
- Enforces publish-before-apply. A local commit is prepared on a clone and
|
||
becomes canonical only once a relay in the group's own scope acknowledges it,
|
||
so a failed publish can no longer fork this client away from the group.
|
||
- Fixes the defects that made us unaddressable: no account could mint a
|
||
KeyPackage at all; KeyPackages were published as bare bytes instead of a framed
|
||
MLSMessage; a Commit rebuilt our own leaf from defaults, silently demoting the
|
||
group creator and making every invite we sent invisible; and a last-resort
|
||
KeyPackage was discarded after one Welcome, so only the first invite anyone
|
||
ever sent us worked.
|
||
- Fixes two MLS defects that meant nobody but a group's creator could invite into
|
||
a group of three or more: a joiner was never given its Welcome path secret, and
|
||
an UpdatePath was decrypted at our own leaf rather than at the node we hold a
|
||
key for.
|
||
- Makes a departure actually take effect. A leaver's `SelfRemove` was committed
|
||
and then dropped, so the group believed they were gone while they stayed in the
|
||
tree holding its keys.
|
||
- Adds group disbanding, with the whole Commit shape the spec fixes, and an
|
||
admin-only action behind its own confirmation.
|
||
- Sends attachments in the adopted encrypted-media v2 shape. Every image Amethyst
|
||
sent into a group was invisible to every other implementation; an admin can now
|
||
also switch a group over to encrypted attachments.
|
||
- Honours disappearing messages, pinned to the epoch that delivered each message
|
||
rather than to the current setting, with a retention picker at group creation.
|
||
- Renders message edits and group system rows, and draws a system row only when
|
||
this client derived it — a peer's assertion of "X removed Y" is no longer drawn
|
||
as history.
|
||
- Adds several members in one commit, the way MDK does, instead of one commit per
|
||
invitee.
|
||
- Adds a group avatar behind a plain https link, with the canonical URL rules the
|
||
component requires and an SSRF guard at the renderer.
|
||
- Adds push token gossip in the shape the spec adopted, with the kind-451 owner
|
||
proof, so relaying someone's record cannot repoint, re-sign or restamp it.
|
||
- Adds agent text stream previews over the repo's own QUIC stack, in both
|
||
directions, with certificate pinning instead of blind trust.
|
||
- Runs the reference implementation's own fixtures, its portable scenario
|
||
vectors, and a headless interop harness against MDK 0.9.21 — 29 of 29, both
|
||
directions, including media in both.
|
||
|
||
### Search
|
||
|
||
- Ports the vespa-relay search field's token language: `from:`/`to:`,
|
||
`since:`/`until:`, `#tag`, `label:`, `group:`, `kind:`, `lang:`, `domain:`,
|
||
NIP-73 scopes, `-exclusions` and quoted phrases, drawn as chips over ordinary
|
||
editable text.
|
||
- Runs them. Android handed the raw box — chips and all — to both the relay's
|
||
NIP-50 `search` and the local scan, so `from:npub1… bitcoin` asked relays for
|
||
the literal text of its own tokens and a `kind:` chip narrowed nothing.
|
||
- Puts both front ends on one `SearchPipeline`, so a caller cannot skip
|
||
post-filters or ranking. Android had never applied `-term` exclusions or the
|
||
reply/media pseudo-kinds, and Relevance sorted by date.
|
||
- Opens a picker under a half-written `kind:`, and gives the people picker the
|
||
composer's own — NIP-05 resolution, relay lookups and follow ranking included.
|
||
- Names what a chip stands for: a group's name instead of its opaque id, a city
|
||
instead of a geohash.
|
||
- Seeds the box with the filter of the screen it was opened from, across 17 more
|
||
feeds, and remembers what you searched for on Android.
|
||
- Says what it is waiting on: the spinner now turns while a relay still owes an
|
||
EOSE, and tapping it lists the relays outstanding.
|
||
- Answers with one kind window derived from Quartz instead of three hand-kept
|
||
lists — the audit found fifteen searchable, renderable kinds the search had
|
||
never asked for, pictures and every video kind among them.
|
||
- Hides deletion (kind 5) and vanish (kind 62) events from results.
|
||
|
||
### Reading: Publications, Library, Wiki and Ratings
|
||
|
||
- Gives a kind-30040 publication a table of contents you can read from, listing
|
||
sections by `a` **and** `e` tag and naming each row from the index alone.
|
||
- Renders kind-30041 sections: AsciiDoc converted to the same CommonMark renderer
|
||
long-form uses, with wikilinks resolved to nevents and naddrs.
|
||
- Gives a chapter a crumb back to its book and a pager through the book.
|
||
- Parses and renders the library kinds — 31/32/33 citations, 30045 directories,
|
||
30142 learning resources and 32176 Blossom piece indexes — and shows a learning
|
||
resource's attached PDF inline instead of as a blue link.
|
||
- Parses and renders the NIP-54 wiki kinds (818 merge requests, 819 acceptances,
|
||
30819 redirects) and kind-17 external reactions.
|
||
- Parses kind-34259 entity ratings and kind-31987 relay reviews, and rebuilds the
|
||
rating card around cover art and stars that actually fill.
|
||
- Renders kind 0 as a profile card, in the feed and in thread view, instead of
|
||
raw JSON.
|
||
- Links each Birdex species to its Wikidata entry.
|
||
|
||
### Payments and Zaps
|
||
|
||
- Offers a NIP-A3 pay-to hand-off in the zap picker, gated on an installed app
|
||
actually resolving the URI and wearing that app's own icon.
|
||
- Hides on-chain zap amounts the wallet cannot fund, checked against the exact
|
||
coin selection the send path uses.
|
||
- Names the payee on outgoing NWC transactions (NWC-06), and only claims a
|
||
binding for a zap request the provider accepted.
|
||
- Never lets a NIP-47 refusal or timeout reach you as silence — a wallet
|
||
answering `QUOTA_EXCEEDED` or `RESTRICTED` showed nothing at all.
|
||
- Stops sending optional NIP-47 params as `null`, which some wallets refuse, and
|
||
stops downgrading to NIP-04 encryption on a cold info cache.
|
||
- Adds a Block Relay button to the relay NOTIFY payment prompt, so a paid relay
|
||
asking on every reconnect can be answered once.
|
||
|
||
### DVM Liveness
|
||
|
||
- Adds DVM heartbeats (kind 11998), hides DVMs whose last beat is older than 15
|
||
minutes from Discover, and marks the rest with an offline dot or banner.
|
||
- Fetches beats from each DVM's own outbox relays, not only the discovery
|
||
selection, so a live DVM your relays do not carry stops looking dead.
|
||
|
||
### Relay Login
|
||
|
||
- Makes the prompt's "remember" switch mean the same thing for both answers. It
|
||
was read only by the Log in button, so refusing with it on wrote nothing and
|
||
the same dialog came back on the next reconnect.
|
||
- Relabels the buttons to the answer they give, and adds "Never, all relays"
|
||
opposite "Always, all relays", both behind one confirmation that names the
|
||
consequence.
|
||
- Stops an account-wide answer being swallowed when the confirmation outlives the
|
||
prompt's 60-second window, and answers the prompts queued behind it.
|
||
|
||
### Everything Else
|
||
|
||
- Shares your screen in a call, on Android.
|
||
- Opens a real file picker for `<input type="file">` — it was a silent no-op in
|
||
the in-app browser, in nSites and in napplets — and offers the camera where a
|
||
mobile browser would.
|
||
- Exposes NIP-44 encrypt and decrypt on the injected `window.nostr`, so a web app
|
||
logging in with Amethyst can seal a NIP-59 gift wrap.
|
||
- Emits the uppercase `D` day-index tags on kind 31923, so calendar events are
|
||
discoverable by date.
|
||
- Notifies on the rest of NIP-34: PR replies, merges, closes, reopens and drafts.
|
||
A merged PR used to arrive on the device and go nowhere.
|
||
- Remembers which side-menu sections you collapsed (#4010).
|
||
- Leaves the always-on notification card at the bare relay count, with the
|
||
per-relay breakdown behind a "Show details" action.
|
||
- Lifts the 3-relay cap on Concord invite links, which blew up the invite button
|
||
for any community with more.
|
||
- Saves videos to `Movies/` instead of `Pictures/` (#4009).
|
||
|
||
## Performance
|
||
|
||
- Draws Amethyst's own icons from a generated font rather than rasterising an
|
||
ImageVector per card: frame duration P90 -10.7%, overrun P90 -17.4%.
|
||
- Defers feed animation transitions until there is something to animate: overrun
|
||
P90 -19.4%.
|
||
- Stops copying whole GIFs into RAM before decoding them. One 69.8 MB GIF cost
|
||
66 MB of heap plus 66 MB of native memory and ~700 ms of pure copying, on every
|
||
scroll back into view.
|
||
- Restores the `ImageDecoder` path for still images, which the same identity
|
||
check had cost every network image in the app.
|
||
- Makes the concurrent-fetch de-dupe actually work — it was rebuilt per request,
|
||
so it never saw a second caller — rate-limits the cache reconciler to once a
|
||
day, and reclaims cache files orphaned by a process death.
|
||
- Stops blocking an OkHttp thread to sign Blossom read-auth, which could occupy
|
||
every per-host slot on a gated host, and collapses a cold burst onto one
|
||
signature.
|
||
- Asks for many addresses in one relay filter instead of one filter each: opening
|
||
a 240-section publication went from a black screen for ~2 minutes at 120% CPU
|
||
to first paint at ~22s.
|
||
- Rewrites Curve25519 field arithmetic on 10 limbs of radix 2^25.5 and runs the
|
||
ladder without allocating: x25519 541us → 121us, and against MDK
|
||
`create_group/1` goes from 4.7x slower to 1.5x while `send_app_message` is
|
||
6.5x faster.
|
||
- Stops `FilterMatcher` allocating once per event — three separate ways, on a
|
||
path that runs tens of thousands of times per keystroke during a search.
|
||
- Adds an allocation-free read path for NIP-50 indexable text to all 133
|
||
searchable kinds, pinned byte-for-byte against the write path.
|
||
- Shares one IME-inset holder per window instead of one per call site.
|
||
- Prices the NIP-19 bracket peel and makes it free.
|
||
- Defers log message construction to the lambda overload, including the loops the
|
||
first sweep's line-anchored greps missed.
|
||
- Bounds the route-text scan by the budget rather than the input.
|
||
- Bounds in-flight on-device AI work and stops blocking IO threads on it.
|
||
- Single-flights the LNURL endpoint resolver: a zap-receipt burst was making ~20
|
||
requests to one provider per user action.
|
||
|
||
## Improvements and Bug fixes
|
||
|
||
- Stops a fresh install stranding on a Tor bootstrap. The watchdog fired once per
|
||
`Connecting` span and a timeout produces no status change, so after exactly two
|
||
attempts it went silent — permanently. Arti now bootstraps on demand instead of
|
||
blocking the whole directory download behind its lifecycle lock.
|
||
- Classifies the app's stand-in relays like the user's own until their lists
|
||
arrive, so a new install stops routing everything over Tor by construction:
|
||
first relay socket moved from login+5.9s to login+1.2s, and events ingested by
|
||
the 20s census from 0–2,590 to 3,719–6,051.
|
||
- Only substitutes default relays when there is no event, not when the list is
|
||
empty — publishing an empty search or inbox list is a choice, and the app was
|
||
overriding it.
|
||
- Stops relay list updates wiping the public entries another client wrote
|
||
(#4075).
|
||
- Recovers IME padding when Compose's insets listener freezes. A back gesture
|
||
could leave a keyboard-sized gap app-wide, permanently, until the activity was
|
||
killed; the correction is now 120ms rather than 400ms.
|
||
- Keeps `TextFieldState` writes on the UI thread, fixing a crash inside Compose's
|
||
undo bookkeeping on the keyboard's own callback.
|
||
- Follows the phone's light/dark setting on the launch splash, and carries a
|
||
pinned LIGHT/DARK choice into it through the per-application night mode.
|
||
- Uses the HLS master playlist and its full quality ladder, sizes the ladder to
|
||
the player rather than to a fixed policy, caps it on metered connections, and
|
||
fixes a picture-in-picture race.
|
||
- Caps MediaSession artwork so the platform never re-scales it — a crash on ROMs
|
||
that recycle the source bitmap.
|
||
- Stops cancelling ML Kit GenAI inference futures, which killed the process from
|
||
a thread the app does not own. The composer cancelled one on every keystroke.
|
||
- Makes the full-screen viewer follow the system bars instead of reserving the
|
||
strip they would occupy, and brings the PDF viewer's chrome to parity.
|
||
- Caps navigation route text arguments. Sending a crash report to the developer
|
||
crashed the app: ICU's regex stack overflows at ~100,000 characters and reports
|
||
"no match" rather than an error.
|
||
- Narrows the `FileProvider` external root to the app-specific directory.
|
||
- Stops deleted lists showing their UUID in the feed filter picker (#3949).
|
||
- Restores GIF insertion on the comment reply screens, where a keyboard GIF did
|
||
nothing and a shared one threw away the reply in progress, and wires it into
|
||
four more composers.
|
||
- Keeps calls alive when the system destroys `MainActivity`, releases the
|
||
screen-capture Surface instead of leaking one per session, and stops reopening
|
||
the camera during teardown.
|
||
- Stops reporting coroutine cancellation as a signer failure, which surfaced as a
|
||
"signer not found" toast on essentially every fast settings toggle.
|
||
- Stops a picker's scrolling from dragging the top and bottom bars with it.
|
||
- Tints the un-boosted repost icon like the rest of the action row, and fixes the
|
||
liked and reposted colours.
|
||
- Stops labelling the mixed media feed "Shorts" — two different destinations in
|
||
the bottom-bar picker carried the same label.
|
||
- Converts Android string escaping to Compose escaping inside the Crowdin sync,
|
||
and gates it in CI, after three syncs in a row reintroduced ~2,900 escaped
|
||
apostrophes that render with a visible backslash.
|
||
|
||
## Desktop
|
||
|
||
- Adds NIP-53 live streaming: a "Live now" section in Discover with ranking and
|
||
search, a per-column live bar on the Following and Global feeds, and a watch
|
||
overlay with player, live chat and live-mode controls, openable from anywhere.
|
||
- Routes kinds 30311 and 1311 into the desktop cache, which had no channel store
|
||
at all, with a 500-message chat cap.
|
||
- Shares the unified search pipeline, and gives the spotlight the same people
|
||
picker — typing `from:` there opened nothing.
|
||
- Surfaces macOS notification-permission errors and times the request out, rather
|
||
than parking forever on a banner the user missed.
|
||
|
||
## Cli
|
||
|
||
- Redesigns `amy status` around who is signed in and what they saved, adding
|
||
relay config, follows, Marmot messages, Concord communities and the operator
|
||
key — and fixing a store report that read 0 events on the default backend.
|
||
- Drives the Marmot work end to end: current-profile group creation, retention,
|
||
media policy and transfer, disband, and `amy marmot stream`.
|
||
|
||
## Quartz
|
||
|
||
- Adds the adopted Marmot profile: the app-data dictionary carrier, account
|
||
identity proof v2, the six group components, the lifecycle state machine,
|
||
branch selection, the bounded convergence pass, the candidate-graph builder,
|
||
encrypted media v2, the kind-451 push owner proof and the agent-text-stream
|
||
codecs.
|
||
- Replaces `org.json` with `kotlinx.serialization` across the remaining sources.
|
||
- Backs linuxX64's `LargeCache` with a striped-lock hash table instead of
|
||
copy-on-write, which was O(n) per put and not thread-safe. Filling 100k entries
|
||
goes from 70ms to 15ms with 1 GC instead of 24 — and the whole linuxX64 suite
|
||
now passes (3,495 tests, from 78 failures), so the CI leg runs all of it.
|
||
- Matches `java.net.URLEncoder` on native and drops the `urlencoder` dependency.
|
||
The two implemented different specifications, so Android and iOS emitted
|
||
different bytes for the same magnet link or NWC deep link.
|
||
- Routes the roles the NIP-50 search extractor was stranding in the body tier: 19
|
||
kinds gain a branch, including the marketplace and Podcasting 2.0 families
|
||
whose titles could never reach a title band.
|
||
- Gives the Trusted List member score a 0–100 scale, reads the generic Trusted
|
||
List entry in a NIP-85 Treasure Map, carries Map entries in both halves of the
|
||
10040, and indexes Trusted List titles for NIP-50 search.
|
||
- Holds a transport failure as provisional for one retry when a relay drops the
|
||
socket between our EVENT and its OK, and restores pool membership first so the
|
||
retry dials something.
|
||
- Waits out the second instead of stamping `created_at` in the future when
|
||
superseding a replaceable event.
|
||
- Adds `DvmHeartbeatEvent` (kind 11998) and the publication, library, wiki and
|
||
rating parsers.
|
||
|
||
## Build & Documentation
|
||
|
||
- Bumps Kotlin to 2.4.20, Compose Multiplatform to 1.12.0 and AGP to 9.4.0, plus
|
||
the rest of the version catalog.
|
||
- Executes the commons migration sweep: 38 relayClient files, the OkHttp client
|
||
stack, 37 model and service singles, the top-nav feed datasource layer and the
|
||
search history move to `commons`; the 12 typealias shims are deleted and their
|
||
1,165 imports repointed; 25 files are promoted from `jvmAndroid` to
|
||
`commonMain` after a per-file concurrency review.
|
||
- Migrates 2,565 string keys — about 105,800 locale entries — to the shared
|
||
Compose resource catalog, and gates the orphaned-translation and escaping traps
|
||
pre-push and in CI.
|
||
- Publishes desktop test reports when the desktop job fails, which was the only
|
||
test-running job with none.
|
||
- Renames the `geode` Homebrew formula to `geode-relay`, since `geode` is
|
||
permanently reserved for Apache Geode, and fixes a style violation in both
|
||
formulae.
|
||
- Adds `marmotBench`, a head-to-head against MDK's own benchmark, reporting bytes
|
||
allocated per operation alongside latency.
|
||
|
||
## Contributors
|
||
|
||
- @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z
|
||
- @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
|
||
- @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c
|
||
- @npub1nxa4tywfz9nqp7z9zp7nr7d4nchhclsf58lcqt5y782rmf2hefjquaa6q8
|
||
- @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5
|
||
- @npub1y9tpxsfnccnnvg7kanqa8wsyxuu2z72wxr2hd3m29psac3dkucdqmm7p4e
|
||
- @npub1gvv9ahktvavf9qjtrgm62le7gplmmchd5usp5wpfhr85hf79kncqj8xchs
|
||
|
||
## Translations
|
||
|
||
- Dutch by @npub1w4la29u3zv09r6crx5u8yxax0ffxgekzdm2egzjkjckef7xc83fs0ftxcd
|
||
- Hindi by @npub1ww6huwu3xye6r05n3qkjeq62wds5pq0jswhl7uc59lchc0n0ns4sdtw5e6
|
||
- Hungarian by @npub1dnvslq0vvrs8d603suykc4harv94yglcxwna9sl2xu8grt2afm3qgfh0tp
|
||
- Polish by @npub16gjyljum0ksrrm28zzvejydgxwfm7xse98zwc4hlgq8epxeuggushqwyrm
|
||
- Slovenian by @npub1qqqqqqz7nhdqz3uuwmzlflxt46lyu7zkuqhcapddhgz66c4ddynswreecw |