Replicates the PollEvent layout: all tag parsing moves to TagArray extensions
in TagArrayExt.kt with the event accessors delegating to them, builder
extensions use addUnique for single-instance tags, and construction goes
through template-returning builders instead of methods that sign internally.
- build() returns an EventTemplate<ContactCardEvent> (the signer is only used
to NIP-44 encrypt the private tags, matching TrustProviderListEvent);
create() remains as the signer.sign(build(...)) convenience and now takes
the emoji list directly
- updatePetNameAndSummary() returns an unsigned EventTemplate; callers sign
it (ContactCardsState and tests updated)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
Nicknames can now use NIP-30 custom emojis: typing : in the nickname dialog
autocompletes from the account's emoji packs, and the emoji mappings for any
shortcode used are embedded in the card's NIP-44 encrypted content next to the
petname — so even the emoji set stays private. Renderers resolve the petname's
shortcodes against the card's decrypted tags instead of the profile's metadata
tags.
- quartz: updatePetNameAndSummary replaces the private emoji tag set wholesale
and keeps it out of the public tags; round-trip test added
- commons: PetName(name, tags) holder with content equality, decryption cache
returns the merged decrypted tag list, EmojiPackState.findEmojiTags resolves
:codes: against the selected packs
- amethyst: Account embeds resolved emoji tags on save; all petname render
sites pass the card tags to the WithEmoji composables; nickname dialog gets
the : emoji autocomplete via EmojiSuggestionState
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
Adds petnames/nicknames per https://github.com/nostr-protocol/nips/pull/761,
reusing the kind:30382 contact card already used for WoT scores — a card only
acts as a nickname when signed by the account's main key. Petname and summary
are always stored in the card's NIP-44 encrypted content.
quartz:
- ContactCardEvent.updatePetNameAndSummary edits both fields in the encrypted
private tags, strips stray public copies, preserves every other tag
- TagArray.petName()/summary() parsers for decrypted tag lists + tests
commons:
- ContactCardDecryptionCache: LRU NIP-44 decrypt cache for own cards
- ContactCardsState: account-scoped access to the account's own cards,
petname flow per target user, create/update entry point
amethyst:
- Account.updateContactCardPetName publishes through the extended outbox
relays (NIP-65 write + private outbox + local + broadcast) and the login
subscription now downloads the account's own kind:30382s from its relays
- petname renders instead of the display name in usernames, profile header,
chats and @mentions (observeUserPetName)
- Edit-nickname action + dialog on the profile actions menu
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
Reverts the Kover coverage aggregation (introduced in dca345212b) from every
module, the root aggregation block, the Sonar coverage import, the version
catalog, and BUILDING.md. With the plugin present the 16 GB CI runner OOM-killed
during the Android test+build job even with Kover opt-in disabled; removing it
entirely clears the OOM. No other CI changes — the diff versus main is exactly
the inverse of dca345212b.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Apply Kover (Apache-2.0, build-time only) to the KMP/JVM modules and aggregate at the root
- include amethyst playDebug in the aggregated Kover coverage
Clients in the wild publish structurally wrong values in profile
metadata — e.g. "nip05":{} — and the strict field serializers made
JsonMapper throw, so contactMetaData() returned null and the entire
profile (name, picture, about…) was discarded.
Generalize the BirthdayTolerantSerializer precedent: string fields now
use TolerantStringSerializer (accepts any JSON primitive, ignores
objects/arrays/null) and the bot flag uses TolerantBooleanSerializer,
so a single malformed field is skipped instead of being fatal.
Non-JSON content (e.g. "Relay initialized") remains unrecoverable and
still parses to null; a test pins that it does so without throwing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSCGp6KgcnaepEMbxNroYe
The READ_OUTBOX relay-auth prompt and the settings toggle described the
permission as only loading posts, but authenticating to a follow's relay
also downloads their profile information and post engagement. Update the
prompt title, consequence line, reason label and toggle description to
say so.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VmzSAtkFv29KWDzPVBbxS
Each PoW job now races PoWPolicy.minerWorkers(cores) = cores/2 parallel
searches (at least 1) instead of a single thread, so a post on an 8-core
phone mines ~4x faster while the other half of the cores stays free for
the UI, and the total CPU budget matches the old 2-job x 1-thread pool.
- PoWPolicy.minerWorkers(availableProcessors) is the single definition of
the per-job worker budget.
- PoWPublishQueue gains minerThreads (used by its built-in miner and
exposed for custom mine lambdas); AppModules wires maxConcurrent = 1 +
minerThreads = half the cores.
- PoWNostrSigner gains a workers param (default 1), covering reactions,
reposts and reports via Account.miningSigner; the anonymous-post paths
in the short-note and comment composers pass the same budget.
- Gift-wrap envelope mining stays single-threaded: the template
conversion is a non-suspend hook inside the synchronous NIP-59 build.
- UI time estimates (settings picker, composer chip, broadcast banner)
now benchmark at the queue's worker count via deviceHashesPerSecond(),
and PoWEstimator caches one rate per worker count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhaF5scnAvhP9wr9R7bTWM
The miner enumerates the nonce space deterministically (the random base is
overwritten before the first hash), so naively racing N copies of the search
duplicates the exact same candidate sequence N times. PoWMiner.mine() now
races workers over disjoint slices instead: each worker's nonce carries a
distinct fixed prefix while only the bytes after it are enumerated, making the
aggregate hash rate scale with cores (~3.8x on a 4-core box).
The hot loop also switches from sha256() to sha256Into() with a reused
32-byte buffer, so hashing no longer allocates per attempt.
amy wiring:
- `pow mine` and `post --pow` mine on all cores by default; `pow mine
--threads N` overrides.
- `pow bench` measures the all-cores rate (what mining now uses, also the
basis for expected_seconds) alongside a new hashes_per_second_single_core.
- PoWEstimator benchmarks with sha256Into to match the miner, and gains a
workers overload that prices in cross-core contention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhaF5scnAvhP9wr9R7bTWM
The PoW publishing-queue branch split broadcastPrivately into a
NIP17Factory.Result overload delegating to a List<GiftWrapEvent> one,
while the DM-unread branch appended markDmRoomAsRead(signedEvents.msg)
to the old single body. The merge left that call inside the wraps
overload, where signedEvents does not exist, breaking compilation.
Move the read-marker call to the Result overload, and mark the room as
read on the PoW early-return path too (the rumor is signed inline there
and the wraps only publish after mining), so PoW-enabled accounts keep
the unread-clearing behavior from #1286/#1287.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1hu9zpyiuJo6yyoJs1vdw
The mining banner's indeterminate bar becomes determinate: each mining job
gets its own bar that fills over the device's estimated duration for its
difficulty (elapsed / (2^bits / benchmarked hash rate)), with an
"≈ 10 minutes left" label next to the elapsed time. The nonce search is
memoryless, so past the mean the bar honestly falls back to the
indeterminate sweep with "any moment now" instead of parking at 100%.
Queued-only banners keep the shared activity sweep.
The mining notification mirrors this: single-job progress fills toward the
estimate with the time-left text, refreshed every 30 s while mining (the
queue only emits on state changes, not clock ticks).
The composer difficulty menu now prices each option on this device —
"24 bits · ≈ 45 seconds" — using the same cached benchmark, and the duration
formatting is shared (formatApproxDuration/formatTimeLeft) with the settings
estimate instead of being private to the settings screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
The per-post PoW button on the New Post screen now sits before the
Subject/Title toggle instead of last in the options row.
Every PoW surface (composer button, note chip, mining banner, settings tile,
relay-info minimum-PoW row) switches from the Bolt icon to Manufacturing
(gear, U+E726): Bolt is the app's zap/lightning glyph everywhere else, so a
Bolt PoW badge next to the zap-split/zapraiser/invoice buttons read as a
payment feature. Subset font regenerated for the new codepoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Shows a button in the relay information header that opens the relay's
NIP-29 group list (RelayGroupServer route) when the relay's NIP-11
document advertises support for NIP-29. Also converts the header button
row to a FlowRow and marks button labels single-line so labels no
longer break mid-word (e.g. "Member/s") when the row runs out of
width.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012w9KridAbRpesbX2ytdjn4
GiftWrapEvent.create no longer takes powDifficulty/powIsActive — it takes a
single templateConversion hook ((template, ephemeralPubKey) -> template,
default identity) that runs on the finished wrap template right before the
ephemeral key signs it. The hook receives the ephemeral pubkey because the
NIP-01 id a nonce commits to includes it and the key never leaves create().
NIP17Factory forwards the same hook through wrapSeal/createWraps and the
create*NIP17 entry points, so quartz's NIP-59/NIP-17 code no longer imports
the NIP-13 miner at all; Account builds the mining closure at the call site.
Any future pre-sign wrap adjustment flows through the same seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Durability & correctness:
- Keep the job entry + disk checkpoint alive until the publish continuation
completes (was: dropped at mining-complete); failed publishes keep their
checkpoint for restart retry and surface through a failures SharedFlow
- Move draft deletion into the publish continuations in every composer so a
cancelled or process-killed mining job can't destroy the only copy of a post
- Persist gift-wrap mining: split NIP17Factory into createSeals (signer
interaction, runs inline) + wrapSeal (pure CPU, runs on the queue), new
REPLAY_WRAPS records restore pending DM wraps after process death
- Clamp synced NIP-78 difficulty (PoWPolicy.MAX_DIFFICULTY), require a sane
target in PoWMiner, bound PoWRankEvaluator against short ids
- Reaction double-tap while mining now toggles (dedupeKey + cancelByKey)
instead of publishing duplicates
- Restore checkpoints for every loaded account, not just the active one
- logOff purges the account's checkpoints and cancels its queued jobs
- Private notes: composer chip now gates on the gift-wrap kind and the
per-post override reaches sendPrivateNote
Coverage:
- Public/live chat (kinds 42 + 1311) and voice replies (1244 + kind-1 audio
replies) now route through the mining gate
Perf:
- FGS start() dedupes with a running flag; PendingIntents built once
- Banner 1 Hz clock only ticks while a job shows elapsed time
- PoWEstimator benchmark is single-flight behind a Mutex
UX / l10n:
- Post-mining failures toast with retry information
- Count strings converted to <plurals>; elapsed time via DateUtils; settings
estimate uses localized units; shared powKindLabelRes replaces three
duplicated kind→label maps; dead pow_chip_* strings removed
- CLI: pow mine lowercases the pubkey before mining (uppercase hex mined an
id that never matches the signed event) and validates via quartz Hex
New queue tests: checkpoint lifetime, failure reporting, cancelByKey toggle,
per-owner cancellation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Addresses the issues found reviewing the #1286/#1287 fix:
- Centralize the room read-marker route in privateChatLastReadRoute()
and the authorship rule in chatMessageMarksRoomAsRead(); all writers
and readers now share one implementation.
- Exempt notes-to-self rooms from the authorship rule — there the
user's own messages are the content still to be seen.
- Mark own chat messages as read at the ingestion choke point
(EventProcessor), covering every send path (including the play-flavor
App Functions assistant) and own messages arriving from other devices
at the persisted-marker level, not just visually.
- Advance the send-path marker to the newest known room message, so a
quick-reply to a skew-ahead peer also clears the indicators.
- Revert the authorship guard on public-chat/ephemeral/marmot rows:
in multi-party channels 'I posted last' does not imply the earlier
backlog was seen, and no marker advances there to back the guard.
- Restore the new-items bubble for rooms whose newest item is an
unsent draft — a draft still needs the user's attention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqN8cfqNdo1MAvLN4C9krm