Add docs/changelog/v1.13.00.md — the v1.13.0 "Web Apps, Communities & Git"
release notes — and link it at the top of the changelog index.
Covers the full v1.12.6..HEAD range: the in-app Browser and NIP-5D/5A web
clients, Concord communities, NIP-34 git collaboration, Location Channels, the
NIP-46 Remote Signer and Privacy Lock, NIP-42 relay login permissions, NIP-29
Groups, the redesigned Messages inbox, Blossom file sync, Podcasting 2.0,
proof-of-work (NIP-13), negentropy (NIP-77), GrapeRank, accent theming, and the
Desktop, amy CLI, Quartz, and Geode work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrPNt4FchqArpMtAHHfqGi
- "Use my location" could spin forever after a permission denial: the reset was
in an isGranted-keyed effect that never re-ran on false→false. Use
rememberPermissionState's result callback, which fires on grant AND denial, so
the spinner always clears.
- Forward-geocode search ran the blocking Geocoder on the UI thread on Android
< 13 (ANR risk). Run the search on Dispatchers.IO.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
The geo-post composer showed two locations: the static external-id marker at the
top and the new "Posting to · Change" row below the message. Drop the duplicate
and, for the geohash case, render the interactive channel control (with retarget)
in the marker's place. Non-geohash external ids still use DisplayExternalId.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
1) Follow this location from the feed: when a top-nav Geohash filter is active
(e.g. after teleporting), the filter header shows a bookmark toggle to
follow/unfollow that place (kind 10081), so a teleported spot can be saved to
your locations — the "add to my interests" step. Respects read-only accounts.
2) Retarget a geo-post: the geohash comment composer now shows "Posting to <place>"
with a Change action that opens the map picker and re-scopes the post's NIP-73
geohash channel (CommentPostViewModel.geohashScope/setGeohashScope), instead of
being locked to the feed's channel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
Add a "Teleport to a place…" entry to every top-nav feed filter that offers
"Around Me" (home, video, discover, notifications, products, music, git,
podcasts, relay-group discovery, …). Selecting it opens the shared map picker;
on confirm the chosen place becomes this screen's TopFilter.Geohash feed.
Implementation is centralized in FeedFilterSpinner (no per-top-bar changes): a
UI-only TopFilter.TeleportPicker sentinel is intercepted there to open the
picker, then forwarded through the normal onSelect path as a TopFilter.Geohash —
which each screen's existing changeDefault*FollowList already handles. The
spinner header falls back to the raw selection so a teleported (unfollowed)
geohash still shows its place name.
Follow-up (not in this commit): a "follow this location" action so a teleported
place can be saved to the kind-10081 geohash list from the feed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
If the picker opens with no seed and location permission is already granted, fly
to the user's current position instead of the neutral world view. Runs once and
never prompts — it only consumes already-granted permission; on denial or no fix
it stays at the world view as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
- Mute the busy MAPNIK tiles in light mode (desaturate + gentle lighten), the
light-mode counterpart to the existing dark tile filter. Applied to both the
interactive picker and the preview thumbnail.
- Outline the selected geohash cell on the map so the user sees exactly which
region a post/filter covers; the rectangle follows the pin and resizes with the
area-size level.
- When the area-size level changes, animate the zoom so the new cell is framed.
LocationPickerMap gains zoomTo/highlight/highlightColor params (all guarded to
rebuild only on change, preserving the per-frame-cheap update path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
Adversarial audit of the PR's own changes (8 finder angles, verified
before fixing). Quartz core:
- fetchAll-family drains get a wall-clock ceiling (maxTotalMs, default
10x the idle window, delay()-watchdog: cancellable and virtual-time
testable). The pure idle window was unbounded when a relay trickled
events forever — sandboxed napplet queries, set -e fetches, and
marmot await stuck inside one drain. Streaming relays still finish.
- The suspending onEvent hook no longer runs inside a cancellable
timeout scope (an expiring window could cancel verifyAndStore
mid-write and silently drop a received event); the timeout is armed
only when the channels are dry (no per-message timeout-job churn).
- fetchAll is a projection over fetchAllWithHooks: fixes its
unsynchronized events/seenIds mutation from concurrent socket
threads and deletes the duplicate loop + per-event activity channel.
- publishAndConfirmDetailed regains its only-responders contract
(synthetic no-response entries no longer render as 'relay rejected
your message' in app callers); results built by pure associateWith;
shared failure-reason constants + PublishResult.isTransportFailure.
- NIP-65 mutations: split read+write r-tags for the same URL now merge
to BOTH instead of last-wins dropping a facet (+ test).
- TcpProber's 128-thread pool drains after 60s idle.
CLI:
- publishGuard: all-transport failure exits 124 as timeout; rejected/1
is reserved for an actual OK-false answer.
- --help anywhere in argv is hoisted centrally; 'amy notes post "x"
--help' prints usage instead of publishing.
- rejectUnknown false-reject traps fixed: geochat --no-fetch behind an
early return, and 13 elvis-alias short-circuit sites read eagerly.
- Aliases load once per Context and only match name-shaped inputs (no
shadowing a real npub/NIP-05/hex); stderr color requires a
positively-known terminal (TERM sniff polluted captured logs).
- Relay-CSV strictness unified on RawEventSupport.relayFlag (post,
graperank publish/followers/register no longer silently drop
malformed URLs); Args.timeoutMs(+OrNull) replaces 27 hand-rolled
conversions, all strict; offer/debit --timeout > 3600 rejected with
a 'looks like milliseconds' hint; NPub.create idiom; stale jq .id in
the marmot reactions harness; printUsage drift (offer pay --with,
profile --clink-offer, search --kind).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
- Open the seeded picker at a zoom that fits the geohash cell's precision
(region opens wide, building opens tight) instead of a fixed zoom.
- Hide the search field's rectangular outline so its square corners no longer
poke through the rounded pill (border colors → transparent).
- Drop the 48dp minimum touch-target on the area-size chips so wrapped rows sit
8dp apart (matching the horizontal gap) instead of ~24dp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
- The interactive picker map didn't tell ancestor views to stop intercepting
touches, so a horizontal drag on the Teleport map got stolen (opening the nav
drawer) instead of panning. Add the same requestDisallowInterceptTouchEvent
touch listener LocationPreviewMap already uses.
- The precision chips were a single horizontally-scrolling Row; make them a
FlowRow so "Region · ~1250 km … Building · ~38 m" wraps onto multiple lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
Bugs:
- "Use my location" could spin forever when a fix never arrives (location off,
indoors, permission granted but unavailable). Wait with a 20s timeout and stop
on any terminal outcome so the button always resets.
- With no seed, an initial osmdroid layout-scroll at the opening center could be
mistaken for a pick, auto-selecting the mid-Atlantic and enabling Confirm before
the user moved. Gate selection behind a real pan/search/GPS/tap (hasSelection).
Performance:
- The map's AndroidView update ran setColorFilter + overlays.removeAll +
invalidate() on every scroll-driven recomposition. Move the dark-mode tile
filter to a theme-keyed effect, and rebuild the marker + invalidate only when
the marker point actually changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
The fetchAll/fetchAllWithHooks accessories wrapped their whole
collection loop in one withTimeoutOrNull, so a relay actively
streaming a large backlog was cropped mid-delivery the moment the
absolute deadline hit — even though the loop already has proper
terminal conditions (per-relay EOSE / CLOSED / cannot-connect) and the
timeout's only real job is stall detection.
timeoutMs now measures the delta since the LAST message: every event
or terminal signal resets the window (fetchAll gains a conflated
activity ping so event progress is visible to its wait loop), and only
a full window of silence ends the fetch early. fetchFirst/count keep
absolute waits (single-response — idle and absolute coincide), and
subscribe's duration timeout stays absolute by design (a live stream
has no terminal state).
Since the pages/pool helpers delegate to fetchAll, pagination inherits
the semantics. This also changes app-side callers of these accessories
— in their favor: the timeout only ever fired on slow relays, exactly
when cropping loses data.
New commonTest suite pins the behavior: a relay emitting every 200ms
under a 300ms window streams to completion (10/10 events); a stall
ends one window after the last message, not after the start; EOSE
still returns immediately. CLI docs reworded (--timeout = idle window).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
With no external consumers yet, converge the --json surface to its
ideal shape in one pass:
- quartz gains publishAndCollectResults: the NIP-01 OK message, connect
errors, and silent timeouts now survive as PublishResult(accepted,
message) per relay instead of dying in a debug log. The existing
boolean APIs delegate unchanged. Silent relays are reported as
'no response within timeout' rather than omitted.
- Context.publish returns the rich map; the new
RawEventSupport.ackFields(ack) is the one canonical projection every
publisher emits: published_to (urls) + rejected_by as
[{relay, reason}] — 'why didn't it post' now answers itself, in
partial failures and in the rejected error alike.
- author/pubkey rule enforced module-wide: 'author' is the key that
signed an event (feed/search/dm/message list items), 'pubkey' an
identity being described; profile show and outbox add the bech32
npub beside the hex when the user is the primary subject.
- Event-list items converge on event_id/author/created_at/content
(dm, feed, search, marmot message, geochat, concord).
- Byte counts standardize on *_bytes keys (blossom/nsite size ->
size_bytes); the text renderer drops the fragile bare-'size'
heuristic and colors stderr progress independently of a piped
stdout.
- Error details are sentences everywhere (not bare gids); dead Result
class removed from the quartz publish accessory.
Docs updated (DEVELOPMENT output conventions, README rejected example).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
Lift pickedGeoHash into the shared ILocationGrabber interface and add a shared
GeoHashPostSection composable (GPS default + "pick on map"), then wire it into
every remaining composer: long-form, classifieds, private DMs, public messages,
NIP-22 comments, channel messages, and nest messages. Each ViewModel now honors
the map-picked geohash over GPS at build time and round-trips it through drafts;
each screen renders the shared section in place of the GPS-only LocationAsHash.
The short-note composer is refactored onto the same shared section (its local
copy removed). The geohash-chat "New location channel" screen already reaches the
picker via its Teleport card, which now uses the shared, polished picker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
The 1500-line GrapeRankCommand (15 sub-verbs in one object, 7.5x the
module's 200-line smell threshold) becomes a 165-line dispatch that
delegates to graperank/{Crawl,Score,Publish,Operator,Support}. The TCP
reachability pre-probe with its dedicated 128-thread dispatcher is
transport infrastructure, not command code — it moves to quartz
nip66RelayMonitor/reachability (TcpProber, jvmAndroid). Pure move, no
behavior change; cli tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
The docs had drifted badly behind the code: geochat was documented
nowhere, concord/zap/search/podcast20/nsite-publish were README-
invisible, the ROADMAP matrix contradicted its own nak table on six
shipped features, and DEVELOPMENT described the legacy FS event store
as the default when SQLite is.
- README: sections for search, zap (incl. --with auto-pay), podcast20,
nsite/napplet (all four sub-verbs), concord (13 verbs), geochat, and
a 'Which chat system?' comparison table; output section rewritten for
the new contract (exit-code derivation, rejected, unknown-flag
errors, -- terminator, per-command --help); layout diagram fixed for
the SQLite default + operator/ + concord.json; the bunker nak-interop
claim reworded honestly; RECIPES.md linked.
- ROADMAP: stale new-item rows flipped (follow, outbox, Blossom,
bunker, search; zap partial), rows added for relaygroup/geochat/
concord/nsite/napplet/podcast20/CLINK/fof, orphaned thread note
fixed, test-suite section updated.
- DEVELOPMENT: canonical error-code list pinned, exit-code rule
documented, no-prompts carve-outs, refreshed architecture tree +
command template (USAGE/route(help=)/rejectUnknown/publishGuard),
SQLite store section, testing table covers the new JVM suites.
- tests/README: all ten suite dirs listed, JVM contract suite noted,
mis-spliced marmot row repaired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
The composer could only attach the current GPS location (fixed ~5km), which
fails when GPS is denied/unavailable and can't tag a post with a different
place. Add a "pick a place on the map" action that opens the shared
GeohashLocationPickerDialog and stores the result in
ShortNotePostViewModel.pickedGeoHash, which overrides the GPS fix at build time
and round-trips through drafts. Picking a place also skips the GPS permission
prompt. The existing "use my location" GPS flow is unchanged; this is additive.
Scoped to the short-note composer for now; the other ILocationGrabber composers
(long-form, classifieds, DMs, comments, channel/nest messages) can adopt the
same pickedGeoHash override + section in a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
Context.drain/drainAllPages/requestResponse re-implemented the
subscription state machine quartz already ships in
relay/client/accessories — the CLI-specific needs (per-event
verify-and-store hook, dead-relay collection, pending-on-auth) now live
in an option-rich fetchAll variant there, and Context keeps thin
adapters. The per-domain sections bolted onto Context (Cashu seed
warming/snapshot/restore counters; the Concord stream-key AUTH
registry) move to CashuContext/ConcordAuth, with the NUT-09 restore
counter rule shared via commons CashuWalletOps so the CLI and Android
can't drift. Context.kt: 1246 -> 950 lines; behavior unchanged
(cli tests green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
Extract the picker body into GeohashLocationPickerContent (chrome-less: map +
search + use-my-location + precision + readout + confirm), leaving
GeohashLocationPickerDialog as a thin dialog wrapper around it.
Rewrite GeohashTeleportScreen to host that shared content inside its own scaffold,
passing a teleport-specific confirm label and action (follow the cell + open the
chat with the teleport flag). This dedupes the map/precision/city-name code that
Teleport previously copied, and gives Teleport place search, "use my location",
dark-mode tiles, area-size chips, and the animated pin for free.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
Regenerates the shipped native library from the preceding lib.rs change. Kept
as its own commit so the binary diff is isolated and can be audited against the
source change independently.
Built with the canonical reproducible-build path (/tmp/amethyst-arti-build) and
the pinned 1.94.1 toolchain; ARTI_VERSION and Cargo.lock are unchanged, so
lib.rs is the only input that moved. Note that the build path is embedded in
the output, so overriding ARTI_REPRO_DIR produces different bytes — the
artifact committed here comes from the canonical path.
verify-reproducible.sh passes: two clean builds produced identical bytes.
arm64-v8a 3293f9fd1663f9481972f4b705fe80feb9650271b5a3b4258420501d6375bb3e
x86_64 a5a5864741a09708cd0b6e44ef3ba28298b4862872084d98e172487d6b12720b
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The JNI wrapper answered every failed client.connect() with SOCKS reply 0x05,
so a domain that no longer exists, an exit that timed out, and a genuinely
refused port were indistinguishable. Java renders 0x05 as
SocketException("SOCKS: Connection refused"), so the whole failure taxonomy
collapsed into one opaque string and callers could only apply their most
generic retry policy. On a cold start with the default settings — torType
INTERNAL and newRelaysViaTor true, so the entire outbox fan-out is routed
through Tor — 639 of roughly 768 relay failures arrived this way.
This is why the DNS classification added earlier was effectively dead code for
default users: name resolution happens at the exit, so UnknownHostException is
never raised locally.
socks_reply_for() maps ErrorKind onto the codes Java surfaces with distinct
messages, so a caller can tell "this relay is gone" from "this circuit had a
bad minute". No new dependency — arti-client re-exports ErrorKind and HasKind.
RemoteHostResolutionFailed is mapped to 0x04 even though Arti documents it as
retryable, because an exit's resolver failing is not proof the name is dead.
The caller's response to 0x04 is a bounded backoff rather than permanent
condemnation, which is a retry, just a slower one; probing the relays that
produced this error found 17 of 21 to be NXDOMAIN from an ordinary resolver, so
the conservative reading costs far more than it saves. Changing that arm to
0x01 restores Arti's reading if that tradeoff is ever judged wrong.
Verified on device: Java now reports "SOCKS: TTL expired", "SOCKS: Host
unreachable", "SOCKS: Connection not allowed by ruleset" and friends where
everything was previously "SOCKS: Connection refused".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nothing could answer "what did every relay in the pool actually do, and why did
the ones that failed, fail". RelayStats has counters but no dump path and no
failure taxonomy; RelaySpeedLogger counts events per second, not connection
outcomes; RelayLogger prints one line per event, which at the few-hundred-relay
cardinality of the outbox model is thousands of lines to grep rather than a
table to read.
BootRelayDiagnostics buckets connection failures by cause (separating a Tor
SOCKS refusal, which says nothing about the relay, from DNS, TLS, an HTTP
upgrade rejection, a timeout, or a genuine refusal), counts REQ/EOSE/CLOSED per
relay with CLOSED split by NIP-01 machine-readable prefix, and records
time-to-first-open and time-to-first-EOSE. It dumps a rollup plus two tables at
20/45/90s: the relays that cost dials and returned nothing, and the relays
actually carrying the boot — the latter so a suppression change can be checked
for coverage loss instead of only counting CLOSED reduction.
Debug builds only, attached like the other loggers in AppModules.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A relay whose domain no longer exists (lapsed registration, decommissioned
host) was treated like a busy relay: the backoff doubled from 1s and spent
about ten dials climbing to the ceiling it was always going to reach. An HTTP
upgrade rejection already jumps straight there; a name that does not resolve
deserves the same.
Matching is on the exception type rather than the message because the message
is localized and platform-specific — Android says `Unable to resolve host "x"`,
JVM on macOS says `nodename nor servname provided, or not known` — while the
class name is stable. That is why onCannotConnect appends it in the first
place. Neither message ends with "Host unreachable", so the existing check
never caught DNS failures.
Being this eager is only safe because the verdict is cheap to revisit: a DNS
answer is a property of the network, not of the relay (a captive portal or a
filtering resolver forges NXDOMAIN), and both a network-identity change and a
transport change now clear the backoff outright. The test pins that round trip,
not just the classification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A relay's reconnect backoff was process-global and network-blind: the delay
earned on one network was still being served out on the next. The only reset
signal was OkHttpClient reference identity, which is rebuilt off the metered
bit, so it fired for wifi<->cellular and nothing else. Wifi A -> wifi B, a VPN
coming up, a captive portal clearing, and metered-wifi -> cellular all left
every relay parked on a penalty earned against a network the device had left —
up to five minutes of silence on a network that might reach the relay instantly.
The transient Off that would have reset things is swallowed by the 200ms
debounce in ConnectivityFlow, so it never rescued those cases.
Key the decision on ConnectivityStatus.Active.networkId instead, which is the
same signal SurgeDns already uses to stale its cache, and treat a genuine
network change as a full pool rebuild: every socket is bound to an interface
that no longer carries traffic, and needsToReconnect() cannot see that because
it only compares the proxy and the timeouts.
Also treat a Tor policy flip as a transport change. Flipping a Tor toggle while
Tor is already up leaves both OkHttpClient references identical, so a relay
whose transport just changed kept waiting out a backoff earned on the other
transport. Only TorRelaySettings is compared, not the relay sets that
TorRelayEvaluation also carries — those churn while an account's relay lists
load (observed firing three times in one cold start), and forgiving the whole
pool every time any list updates is far more damage than it repairs.
Adds IRelayClient.resetBackoff() (default no-op, so the existing fakes and
BleNostrClient are unaffected) rather than reusing ignoreRetryDelays, which
only skips the gate for a single attempt and still doubles the stored delay —
a relay that failed that one dial came back worse off than before.
INostrClient.resetBackoff() is deliberately separate from reconnect(): the
latter debounces, so folding this into a coalescing command would let a later
request silently drop the reset.
The decision table moves into RelayProxyClientConnector.apply() so it can be
exercised directly, without a debounce and a shared StateFlow in the way.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refinements after the initial map picker:
- Dark mode: apply the night tile filter to the interactive LocationPickerMap
too, so the picker matches the app theme instead of showing a bright map.
- Search results: the place search now shows a tappable list of candidates
(with human place names) instead of silently jumping to the first hit.
- Precision chips now show approximate area sizes (e.g. "City · ~5 km").
- The center pin lifts with an animated shadow while the target moves.
Generalization: the picker's own strings are renamed from relay_group_location_*
to neutral location_picker_* keys so any caller can reuse
GeohashLocationPickerDialog, not just the NIP-29 group form. The group-form card
strings stay namespaced. LocationPreviewMap gains a configurable aspectRatio.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
Seven end-to-end recipes (own relay + NIP-86 admin, NIP-46 bunker both
directions, Marmot group chat, NIP-60/61 Cashu wallet, NIP-5A nsite
publishing, GrapeRank provider pipeline, scripting patterns) so the
docs teach jobs, not just verbs. Reflects the new contract: per-command
--help, alias resolution in user slots, published_to/rejected errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
Reorder the App Settings section in AllSettingsScreen to: Privacy Options,
UI Preferences, Home, Messages, Notifications, Compose Settings, Reaction
Row, Bottom Navigation Bar, Profile UI, then the remaining rows.
Rename the "Home Tabs" row to "Home" (reusing route_home) and switch the
Messages row icon to MaterialSymbols.Mail to match the Messages screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYck7FKoLzoaomnjjLFLzi
The NIP-29 group create/edit form asked for the group's location as a raw
geohash text field (placeholder "u0nd") — unusable, since nobody knows their
geohash, so the discovery geo-filter it feeds stayed empty.
Replace it with a first-class, map-first location experience, reusing the
pieces that already power the Geohash-chat Teleport screen:
- New GeohashLocationPickerDialog: a full-screen picker where the user pans a
map under a fixed center pin, searches for a place by name, or taps "use my
location" (device GPS). Precision chips (GeohashChannelLevel) control the
area size; the resolved place name is shown via LoadCityName. Never surfaces
a raw geohash.
- New ForwardGeolocation service (Geocoder.getFromLocationName) powering search,
mirroring the existing ReverseGeolocation.
- Extend LocationPickerMap with backward-compatible recenter/recenterZoom and
onCenterChanged hooks for the center-pin interaction; extend LocationPreviewMap
with a configurable aspectRatio for the form's map thumbnail.
- Rework the form's location field: an inviting empty-state card, a filled card
with a themed map thumbnail + place name + geohash + clear, and a collapsed
"enter manually" field so power users can still paste a known geohash.
The ViewModel's geohash state stays the single source of truth, so the publish
path (parseGeohashes -> kind-9002 EditMetadataEvent) is unchanged. Adds the
MyLocation glyph and regenerates the Material Symbols subset font.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh
The kind:10002 facet-merge rules (adding a write marker to a read-only
relay promotes it to BOTH; removing one facet of BOTH demotes to the
other; removing the last facet drops the relay) lived as private
helpers in the CLI's RelayCommands. Any frontend that edits a NIP-65
list needs them, so they now live in quartz nip65RelayList as
AdvertisedRelayListMutations (applyFacet/addFacet/removeFacet/setFacet)
with commonTest coverage. Behavior unchanged; the CLI rewires to the
shared functions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
Context.syncIncoming carried real protocol policy — the NIP-59 gift-wrap
since-cursor rule (2-day lookback, advance only when events arrive),
per-group cursor bookkeeping, and the MIP-00 consumed-KeyPackage
rotation — that the Android app implements separately. Divergence there
silently drops DMs, so the policy now lives once in
commons/marmot/MarmotSyncPolicy with Cursors/Relays/drain/publish
injected, and the CLI Context wires itself in as a thin adapter.
Behavior is unchanged (the body moved verbatim, comments included).
Also resolves aliases from the per-account aliases.json in
Context.requireUserHex, so 'amy dm send bob ...' works with a local
alias — previously aliases.json was written but never read.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
Same contract as the A-D sweep, applied to the E-M command families:
- rejectUnknown() everywhere Args is constructed (typo'd flags -> bad_args)
- publishGuard on single-event publishes (total rejection -> non-zero)
- USAGE constants + route(help=...) / --help fast-paths; graperank's
full sub-verb set (including the previously undocumented 'followers')
and 'key validate' + the --pw alias are now documented in-binary
- geochat --relay is a strict comma-list (bare positional relays kept);
geochat listen default limit 500 -> 50
- graperank update/probe aliases now print deprecation notes
- git --identifier accepted as alias of --d
- marmot message list defaults to --limit 50 (0 = everything)
- dm-style guards for marmot message send/react/delete and group edits
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
Applies the new CLI contract across the A-D command families:
- rejectUnknown() after flag parsing, so typo'd flags fail with bad_args
instead of silently no-oping
- publishGuard on single-event publishes: total relay rejection now
exits non-zero ('rejected') instead of 0
- per-group USAGE constants wired into route(help=...) and --help
fast-paths on flat commands; previously invisible sub-verbs
(blossom media/report, the concord moderation set, the whole cashu
surface, NIP-86 method list) are now documented in-binary
- dm send/send-file parse flags before positionals (flags may appear
anywhere); dm list/await accept a positional USER as alternative to
--peer; dm list caps at 50 by default
- concord create: --relay is canonical (--relays kept as alias)
- cashu receive resume: deprecation warning pointing at 'complete'
- offer/debit: --timeout is seconds (was raw milliseconds)
- Identity.fromBunkerUri now delegates to quartz NostrConnectURI
parseBunker instead of duplicating the URI parsing
BREAKING (--json): unknown flags and malformed numeric/relay/author
flag values now exit 2 where they were previously ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
- Output.error now derives the exit code from the error code
(bad_args -> 2, timeout -> 124, else 1), so every
'return Output.error(...)' site honours the documented contract.
Previously ~225 bad_args sites exited 1 while the docs promised 2,
and two timeout paths (nostrconnect wait, namecoin lookup) exited 1
instead of 124.
- Args: literal '--' ends flag parsing (escape hatch for values that
start with '--'); intFlag/longFlag reject non-numeric values instead
of silently using the default; requireFlag/positional no longer
double-print to stderr; new rejectUnknown() turns typo'd flags into
bad_args failures; new 'help' detection.
- route() understands --help/-h/help (prints group usage, exit 0) and
names the expected verbs on an unknown sub-verb.
- Unknown or missing top-level subcommand now emits a proper bad_args
error (JSON-aware under --json) plus a one-screen verb list instead
of dumping the full 400-line usage.
- RawEventSupport: --relay/--kind/--author/--id/--since/--until/--limit
entries that do not parse are now bad_args errors; previously an
unresolvable --author was silently DROPPED and the query ran with a
weaker filter than requested. New shared publishGuard() reports
'rejected' (exit 1) when every relay refuses an event.
- runCli() seam extracted from main() plus an 'amy.home' system-property
override of DEFAULT_ROOT so the new JVM test suite can drive the CLI
in-process; first contract tests: ArgsTest, ExitCodeContractTest,
JsonContractTest (NIP-19 vector goldens).
BREAKING (--json): error code pow_timeout is now timeout; exit codes
for bad-argument failures move from 1 to 2 as documented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
The Threads tab loaded kind-11/1111 with a single #h since-filter and no
limit, so a group with more threads than the relay's default result cap
silently lost the older ones. Mirror the chat history stack for threads:
- RelayGroupChannel.threadsHistory: separate RelayLoadingCursors so paging
the forum doesn't move the chat's cursor.
- buildRelayGroupThreadsHistoryFilters: per-armed-relay #h + kind-11/1111
until+limit page, the forum analog of buildRelayGroupHistoryFilters.
- RelayGroupOpenThreadsHistoryFilterAssembler: the on-demand BackwardRelayPager
("relayGroup.threads.history"), bound to the open group's threadsHistory
cursors, landing on the normal ingest path (kind-11 -> addThread).
- Threads screen: mount the history subscription, eagerly backfill to a
window on open, page older content as the list nears its end, and show a
quiet loading/caught-up footer.
Tests: RelayGroupFilterBuildersTest gains the threads-history filter shape;
RelayGroupFilterServingRelayTest gains a geode backward #h + thread-kinds
walk proving every thread is covered exactly once and the walk terminates.
The screen wiring is device-untested (flagged with the other Tier-D items).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU
A group-scoped content event (kind-9 chat, poll, kind-11 thread) is keyed
to its RelayGroupChannel by the relay that served it, because a NIP-29
event doesn't carry its host relay. That's correct for the group's own
host-pinned subscriptions, but a message resolved from a NON-host relay --
e.g. a quoted kind-9 fetched by id during missing-event resolution -- was
filed under GroupId(groupId, strangerRelay), a channel the group's screens
never read, so the message silently vanished (the serving-relay hazard).
LocalCache.attachToRelayGroupIfScoped / attachThreadToRelayGroupIfScoped
now, when no channel is keyed to the serving relay, redirect the stray to
the group's single confirmed host channel via redirectStrayRelayGroupContent,
keyed off RelayGroupChannel.hasRelaySignedState(). A phantom channel never
has relay-signed state, so the redirect can only ever land on a real host,
never on another phantom -- the fix is strictly safe and the common
host-pinned arrival stays an untouched O(1) fast path (the scan runs only
on the rare no-channel-for-serving-relay miss).
Also add the cache-prune gap-fill can't-miss test: drive the production
RelayLoadingCursors down a real relay, rewindTo below the window, and
confirm the pruned band re-loads with no gap.
Tests: RelayGroupContentRoutingTest (pure router + the channel signal);
RelayGroupHistoryPagingRelayTest gains the rewind reload case. The
LocalCache wiring is unit-covered at the router level but still device-
untested end-to-end (flagged in the test plan).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU