retrieve_release now uses tor::http_request (honors route_over_tor:
Tor when on, clearnet when off) instead of the clearnet-only
HttpClient::send. Drops the goblin-wallet User-Agent so the check no
longer self-identifies; the Tor helper supplies a browser-like default
UA that also satisfies GitHub's UA requirement.
The VPN nudge under the Tor switch now renders semibold and one tone
brighter (text_dim instead of text_mute) so the safety recommendation
reads as a deliberate emphasis rather than a muted caption. Styling
only via RichText, so the single localized vpn_note string is untouched
and no retranslation is needed. The shared network_privacy_panels body
means this applies to both the Settings privacy screen and the
onboarding Network-privacy step.
When the Notifications "Hide all details" switch is on, the finer
"Hide amounts" and "Hide names" switches now render forced visually ON,
dimmed, and non-interactive (a new toggle_locked widget plus a
settings_row_toggle_locked helper) instead of misleadingly showing OFF.
The locked rows only render; they never write the hide_amounts /
hide_names config, so the stored values are preserved. Turning "Hide all
details" back off restores the user's real per-toggle choices and makes
the switches interactive again. No copy/locale changes.
A full .backup carried only the seed, identities and active pointer, so a
restore recovered funds via chain rescan but lost the Goblin activity layer
(counterparty npubs/names, tx notes, request records). Rebuild that layer by
sealing the nostr store's activity metadata into the backup under the same
password and merging it back on import.
- ArchiveSnapshot (types): tx_meta + contacts + requests across all
identities — the minimal set the activity list and requests panel join on.
Processed markers, service settings and cached news are excluded (operational
or relay-reproducible). All fields serde-default for forward/backward compat.
- NostrStore::snapshot_archive / merge_archive: merge is non-destructive —
tx meta by updated_at, contacts/requests insert-if-absent, so a restore over
an existing wallet never regresses newer local rows.
- build_full_backup gains an optional history_json, sealed with the existing
two-layer seal_secret_text (npubs/names/notes never appear in plaintext).
open_full_backup surfaces it as FullBackup.history.
- Format stays compatible both ways: the envelope is read field-by-field with
serde_json .get(), so a NEW file still restores on an OLD app (unknown
history field ignored) and an OLD file still restores on a NEW app (absent
history -> None). Omitting history reproduces the pre-history bytes.
- create_full_backup snapshots the store; restore_full_backup_identities (the
single restore chokepoint for settings + onboarding) merges it in.
Tests: history round-trip, legacy-without-history opens clean, new-file keeps
the legacy open-path fields, snapshot->merge into an empty store, and merge
does not clobber newer local rows.
Wiping payment history clears tx_meta (the tx<->npub link), so build_item
already anonymizes a wiped row's title/npub. But the avatar was resolved
from the row's name string independently of the npub, so a leftover name
or cached profile could still render a real profile picture and betray who
a wiped tx was with.
Gate avatar resolution on the presence of an npub association: a tx row
with no npub (wiped metadata, or a non-nostr/plain tx) now renders the
anonymous yellow-goblin tile and never fetches a real avatar, across the
activity feed, receipt and profile-history surfaces. Contacts are kept by
design (the address book) and are documented/asserted as such.
Adds a store-level wipe test (no UI query can rejoin a wiped tx to an npub
or name) and a pure resolution-gate test.
On-device build160 feedback (Android, dark + light):
- Route-through-Tor card: stack it centered so it reads as switching
between Tor and direct. Title on top, the big switch on its own row,
full-width caption below. The subtitle can no longer clip under the
switch at 390px or larger font scales.
- Grow toggle_large from 64x36 to 96x54 (roughly 1.5x) for the switch.
- Grin-node status dot is now the brand yellow in every theme (new
theme::GOBLIN_YELLOW const; theme accent inverts to dark on the yellow
theme, so it couldn't be used here).
- Drop the onboarding "NETWORK PRIVACY" top-right kicker so the privacy
screen shows just the back chip + title, matching the settings
sub-pages (Advanced Privacy, Node). step_header skips an empty kicker.
- Copy (English only, pending owner sign-off): merge the two intro
blocks into one short line and drop the "How your privacy works"
heading; "Usernames" capitalized + "to and from name authorities";
Tor caption shortened to "Hide your IP from relays."; shorter
Grin-node blurb. Stale non-English values left as-is; key set
unchanged (how_privacy / how_privacy_blurb / onboarding.privacy.kicker
kept but unused, so i18n parity holds).
Owner correction: relay.floonet.dev ships in the default lists
(DEFAULT_RELAYS / TOR_RELAYS) but gets no special treatment. All three
defaults are ordinary, fully removable entries: the remove (X) control is
back on every row and a user override is used verbatim, so a saved list
without floonet stays without floonet, per wallet per transport.
Drops pin_floonet() and the forced prepend in effective_relays, and
updates the stale "pinned shared floor" style comments to say defaults
are just defaults and user edits win. The per-transport persistence work
stands unchanged.
The "Nostr Relays" editor saved a single transport-agnostic override that
won on both Tor and clearnet, so a list edited on one network silently
replaced the other. Split the persisted override into relays_tor and
relays_clearnet in nostr.toml so each network keeps its own remembered
list across app updates; the legacy `relays` field stays honored as a
fallback for whichever transport has no explicit set (no migration).
If the user has never edited, behavior is unchanged: the pinned TOR_RELAYS
set on Tor, the per-identity random healthy subset on clearnet.
relay.floonet.dev is now pinned first on every resolved override via
pin_floonet() and can no longer be removed in the editor, so any two
wallets always share a rendezvous regardless of per-wallet edits.
Adds round-trip / precedence unit tests for the config and the pin helper.
Pure move (no behavior change): convert the ~1,985-line session.rs file
into a session/ directory module along its existing banner-marked seams:
session/mod.rs locked constants, the Session object and its
enforcement (decide/serve bookkeeping, rate/size/TTL),
SessionSummary/PendingMoney, and the full test suite
session/classify.rs tier classification and the trust/grant taxonomy
(kind-to-tier table, content escalation, kind-set
sanitation, category display mapping)
session/sign.rs request/channel wire types, the client-pinned signer,
NIP-44 envelope shapes and the serving orchestration
Public API is unchanged: mod.rs re-exports classify and sign items
(pub use), so every crate::nostr::session path still resolves. The shared
abs_diff clock helper lives in the parent so both descendants reach it; no
other visibility changed (descendants already see parent-private items).
Pure move (no behavior change): convert the ~3,400-line client.rs file
into a client/ directory module, splitting the NostrService impl and the
background service machinery along existing seams:
client/mod.rs NostrProfile/HeldIdentityKeys/NostrService/TransportStatus
types + core lifecycle & status accessors
client/sessions.rs Authorize-Sessions surface (add/announce/resume/end,
summaries, money-prompt answering)
client/identity.rs identity/key surface, contact resolution, NIP-05 recheck
client/send.rs DM send path (payment/control DMs, delivery wraps, gates)
client/service.rs run_service loop, relay connect/redial, ingest, expiry
Public API is unchanged: methods stay on NostrService and resolve at the
same crate::nostr::client paths. Cross-module private items widened only to
pub(super) where a sibling calls them. Tests move next to their subjects.
Behavior-preserving structural refactor of the ~8,600-line
src/gui/views/goblin/mod.rs into 17 flat sibling modules following the
existing send.rs/onboarding.rs/widgets.rs pattern. Pure move: no UI,
string, layout, or logic changes.
mod.rs keeps the shell (Tab, GoblinWalletView + state structs,
SettingsPage, Default impl, nav/lifecycle methods, the ui() dispatcher,
and chrome: tab_bar_ui/sidebar_ui/handle_tex/avatar_self) and shrinks
from 8,640 to 1,711 lines.
New siblings (each `use super::*;`, cross-module items pub(super)):
home, pay, receipt, profile, activity, receive, me, settings,
settings_advanced, settings_node, username, identities, prompts, modals,
privacy (network/Tor screen), helpers, format. Unit tests moved next to
their subjects (censor tests -> format::tests; dispatcher test stays).
cargo check --all-targets clean; cargo test --lib 257 passed;
i18n_keys passed. rustfmt applied to touched files.
Enable the SDK's built-in NIP-42 auto-auth explicitly on the wallet's
relay-pool client so it answers a relay's AUTH challenge with a
signer-signed kind-22242 event, then re-issues the pending REQ. It fires
ONLY when a relay challenges (opportunistic): the wallet never forces
auth and never refuses a non-challenging relay, so it is inert against
today's non-challenging relays and only activates once the recipient-only
strfry fork starts challenging kind-1059 reads.
automatic_authentication defaults to true in nostr-sdk 0.44; set it
explicitly so a future default flip cannot silently break DM reads on the
hardened relay. Multi-identity is active-identity-only (single signer per
connection); the builder comment documents the full-multi-identity path
via the pool's forwarded AUTH challenge. Adds an inline test asserting the
opts+signer wiring.
The old privacy_ui used these two headers; this branch replaced it with
network_privacy_panels, which does not. Remove the now-unreferenced keys
from all locales (parity preserved, i18n_keys drift guard still green).
Translate the 16 new per-user-Tor keys and the changed transport-neutral
goblin.privacy.intro across all 9 non-English locales (de/es/fr/ja/ko/ru/tr/
zh-CN/zh-TW), replacing the slice-3 English placeholders. Confirm
goblin.receive.copy was already translated everywhere.
Normalize zh-Hant, zh-HK, and zh-Hant-* system locales to zh-TW (and
zh-Hans(-*) to zh-CN) in setup_i18n, so Traditional-script systems that
don't report the exact zh-TW tag still land on Traditional instead of
falling through to the default locale.
Note the zh-TW -> zh news-locale fold in news_locale_code as an
intentional, low-priority tradeoff (Traditional readers see the same
zh news feed as Simplified); no new pipeline added.
- receive Share/Copy now emit the nprofile (relay hints) so payers learn
which relays to reach the recipient on; in-app npub still tracks who-is-paid
- rename receive button 'Copy npub' -> 'Copy' (goblin.receive.copy) in all
10 locales; copies the nprofile the QR already encodes
- composite the black goblin mark centered on the receive/my-code QR
(widgets.rs, High ECC) and the static qr.rs display (bumped to High ECC)
Phase 2 slice 3 of the per-user-tor plan: the interactive UX layer on top of
slices 1-2 (wallet-level Tor setting + transport-aware relay selection).
Colors:
- Add a tor_purple token (CSS blueviolet #8A2BE2) to ThemeTokens in all three
theme consts, exposed via Colors::tor_purple().
Shared Network-privacy component (onboarding + settings never drift):
- network_privacy_panels(): intro copy (what Goblin sends + how privacy works,
no crypto version numbers), a Grin-node ALWAYS-DIRECT panel (grey dot, full
width), a private-traffic panel whose header is TOR OFF (red) / TOR ON (green)
and whose status dots are goblin-yellow when off / blueviolet when on, a large
Tor switch (w::toggle_large: dormant gray off, blueviolet on, never yellow),
and a VPN nudge. Repurposes the old read-only privacy_ui.
Onboarding:
- New Step::Privacy rendered before wallet creation; the create/restore triggers
route here first. New users default OFF. The choice is stashed on
OnboardingContent and written as the new wallet's EXPLICIT tor setting
(set_tor_enabled) right after open() (init_nostr builds the service
synchronously; start() runs later on the sync thread), so a new wallet writes
an explicit value while upgraders keep None -> Tor ON.
Settings:
- Tor routing row shows On/Off and opens the Network-privacy screen; the big
switch there flips the wallet tor setting (set_tor_enabled + set_route_over_tor)
and restarts the service so the pool rebuilds on the new transport.
Status lines:
- Rewire the 3 Tor-only status call sites (home card, Me identity row,
onboarding identity) to service.transport_status(), adding Connected/
Connecting (direct) states so a clearnet wallet no longer reads as forever
connecting over Tor. Fix the Me-row repaint loop to gate on relay-connected
rather than the Tor-only transport_ready.
- Re-export TransportStatus from crate::nostr (slice-1 miss).
i18n: 16 new keys added to all 10 locales (English placeholder in the 9
non-English files; a following slice translates them).
On Tor, every identity uses a FIXED pinned set (relay.floonet.dev +
relay.nostr.net + offchain.pub). On clearnet, each identity keeps its
persisted random healthy subset (dm_relays), floonet pinned. Route
NostrService::relays() through a new pure effective_relays() resolver
and gate ensure_advertised_set() to clearnet so Tor never overwrites the
persisted clearnet subset. relay.floonet.dev is pinned first in both
regimes. Adds unit tests for the resolver.
Foundation slice for making Tor a wallet-level user setting (Phase 2, slice 1
of the per-user-tor plan). Transport/config only; no onboarding/settings UI,
relay-set swap, or nprofile in this slice.
Wallet-level tor_enabled (tri-state):
- NostrConfig gains tor_enabled: Option<bool> with the existing Option +
serde(default) pattern. tor_enabled() resolves None -> ON, so every legacy
nostr.toml keeps Tor with no migration; new wallets write an explicit value at
onboarding (later slice). Adds tor_enabled_is_set() + set_tor_enabled() and a
back-compat/tri-state unit test.
Clearnet path (the big lift, rebuilt after the Nym removal took it out):
- ClearnetWebSocketTransport parallel to TorWebSocketTransport: direct TLS
websocket, honoring the user's AppConfig SOCKS5 proxy (tokio-socks, already in
the tree) so a VPN/proxy user can front their traffic. Shares ws_config/
split_ws with the Tor transport.
- Transport selection moved into run_service: the wallet's resolved tor_enabled()
picks Tor vs clearnet when the one pool is built; the Tor-bootstrap wait is
skipped for clearnet wallets. restart() re-enters here, so a settings toggle
rebuilds the pool on the new transport.
- Process-global route flag (tor::set_route_over_tor/route_over_tor), mirrored
from the open wallet like set_home_domain, so free-function HTTP callers pick
the matching transport off-thread.
- tor::http_request_bytes takes a clearnet branch when the wallet is Tor-off
(no more hard-fail on "Tor not bootstrapped"); new http::clearnet_request_bytes
reuses the existing HttpClient (proxy-aware) with the same redirect behavior
and browser-like UA. NIP-05, price, relay-pool gist and NIP-11 probes all ride
it unchanged.
Status/readiness:
- New TransportStatus enum + NostrService::transport_status()/tor_routing():
exposes a "Connected (direct)" clearnet state so a Tor-off wallet does not read
as "connecting over Tor" forever. The three UI status lines are rewired to it
in a later slice; the state is exposed cleanly now.
Grin node path unchanged (stays clearnet always). cargo check --all-targets
green; targeted config/nostr/tor tests pass.
Build157 put GoblinView::avatar_self (a tap-through to Settings) top-right
on both the home and pay headers. Per owner request, drop it from the PAY
page only; the home header avatar is unchanged. The scan-QR glyph and the
rest of the pay layout are left in place. Settings remains reachable from
the bottom nav bar (Tab::Me) and the wide-mode sidebar.
Full native-quality Traditional Chinese (Taiwan) locale covering all 910
keys. Shared GRIM-inherited keys use jasperli2026's zh-TC values verbatim;
goblin.* namespace translated from the corrected Simplified source into
Taiwan-idiom Traditional (OpenCC s2twp plus a manual Taiwan post-fix and
review pass), keeping Jasper's voice and crypto-wallet register. Cangjie
keyboard labels reused from zh-CN; lang_name set to 繁體中文.
Wired into the i18n drift test (OTHER_LOCALES) and README attribution;
rust_i18n auto-loads it and the language picker labels it via lang_name.
System locale zh-TW auto-selects; news folds zh-TW to zh (shared with
Simplified) as noted in news_locale_code.
Append ui.add_space(View::get_bottom_inset()) at the end of
SettingsContent::ui. The settings screen supplies no bottom inset (its
tab panel is hidden and the CentralPanel frame uses bottom:0), so the
final FADERS Settings button clipped under 3-button nav / the gesture
pill. get_bottom_inset() is 0 off Android, so desktop is unaffected.
Drop the PoolRelay.exit field and the exit_for/exit_for_host/has_exit
helpers left over from the retired Nym-era co-located scoped exit. The
deleted src/nym was their only real caller; the Tor build never consumes
them. PoolRelay has no deny_unknown_fields, so a gist that still carries
a stray per-relay "exit" key keeps deserializing and the key is ignored
(covered explicitly in the unknown-fields test).
The wallet pivoted from the Nym mixnet to Tor in build134; the Nym code
had been left on disk behind an optional, always-off "nym" feature and
never ran since. Delete it entirely: no Nym/mixnet fallback and no
Nym/mixnet mentions remain. Tor is the sole transport.
- Delete src/nym/ (mod, transport, streamexit, dns, nymproc) and its
`#[cfg(feature = "nym")] pub mod nym;` declaration in lib.rs.
- Cargo.toml: drop the `nym` feature and the commented-out Nym-only
path deps (nym-sdk, smolmix, hickory-proto); rewrite the mixnet-era
comments on the rustls/tokio-rustls/arti/openssl deps to the current
Tor reality. No active dependency changed: rustls, tokio-rustls and
webpki-roots stay (used by the Tor HTTPS client in tor/mod.rs).
- settings/config.rs: remove the persisted nym_entry_gateway /
nym_last_ipr fields and their getters/setters. No serde
deny_unknown_fields, so existing on-disk configs that still carry
those keys keep deserializing (the removed keys are ignored and
dropped on next save; same path the price-cache-removal test guards).
- Rename the Nym/mixnet-named i18n keys (values already said "Tor")
across all 9 locales and their t!() call sites: connected_nym ->
connected_tor, nym_ready -> tor_ready, connecting_nym ->
connecting_tor, mixnet_routing -> tor_routing, over_mixnet ->
over_tor. Displayed strings unchanged.
- Rewrite the Nym/mixnet-referencing comments in the active Tor files
(tor/engine.rs, tor/mod.rs, nostr/client.rs, nostr/pool.rs,
nostr/mod.rs, lib.rs, wallet/wallet.rs, node/node.rs,
gui widgets/mod/onboarding, Android BackgroundService) to describe
Tor; drop the now-broken [crate::nym::*] intra-doc links. No active
code behavior changed.
The pool's `exit` schema slot and exit_for/exit_for_host/has_exit
helpers are kept (comments de-Nym'd): they are inert under Tor but let
a pool document that carries an exit still parse.
The .backup file now seals the money seed AND every held identity in one
encrypted, versioned envelope, and the caption's promise ("Contains your
wallet and all identities.") is finally true.
Format (nostr/identity.rs): a JSON envelope tagged goblin_backup:2. The
seed phrase is sealed with seal_secret_text() and each identity with the
existing per-identity to_encrypted_backup() (so every element is itself a
valid v1 identity envelope). Both layers reuse the in-tree NIP-49 ncryptsec
+ NIP-44 primitives under one password: no new crypto, no new dependency.
The plaintext seed is assembled and sealed in memory (the zeroizing
recovery string drops at scope end) and never written to disk. Old v1
single-identity backups keep restoring unchanged; is_full_backup() is
checked before the v1 path so the two never collide.
Build (wallet.rs): create_full_backup() gathers the seed via the existing
recovery API (which proves the wallet password) plus every unlocked held
identity. restore_full_backup_identities() reinstates them into a freshly
created wallet, re-encrypting each under the new wallet password, making the
backup's active identity active, and reusing the held-index/service-rebuild
plumbing. The identity-only import paths now reject a full backup with a
clear pointer to wallet creation.
Restore (onboarding.rs): the restore-from-seed step gains a .backup picker;
unlocking it decrypts the seed into the standard 24-word creation grid and
stashes the identities, which a worker reinstates once the wallet opens.
Copy: backup caption, title, blurb, and saved-sub updated across all nine
locales; two new restore strings added in all nine.
Tests: full_backup_roundtrips_seed_identities_and_active,
old_single_identity_backup_is_not_a_full_backup,
seal_secret_text_roundtrips_and_is_opaque; i18n drift + news tests green.
Group the Advanced settings page under two grey/red section kickers:
- ADVANCED NOSTR SETTINGS: the Nostr key, and directly below it the
single always-everything .backup download (one button, no checklist).
Moved the .backup off the main Settings identity card so it has one
home (no duplicated exposure). Caption states what the file truly
holds: the current identity's key and username.
- DANGER ZONE (red kicker): Delete wallet, now password-gated. The old
tap-twice confirm is replaced by a wallet-password gate
(get_recovery), with an inline "Download backup" button before the
password field wired to the same seal action. Copy states the delete
also removes every identity and urges a backup first.
Add w::kicker_danger; BackupState.anchor_delete routes the seal form to
whichever section opened it. New strings in all nine locales; clear the
backup password from memory on leaving the page.
Build 157 hard-coded the flat Goblin-yellow censored tile for the user's
own top-right avatar on the home and pay headers, regardless of anonymous
mode. The owner clarified: anonymous mode is the ONLY thing that turns an
avatar yellow.
Replace the mode-blind w::avatar_self widget with a mode-aware
GoblinView::avatar_self helper: anon ON renders the censored yellow tile
(w::avatar_censored), anon OFF renders this identity's normal gradient/
picture identicon (w::avatar_any), matching every other surface. Remove
the now-unused free avatar_self widget from widgets.rs.
All other avatar_censored call sites (identity switcher, activity rows)
remain correctly gated behind the anonymous-mode flag.
A single news post now ships as nine per-`d` language variants and the
publisher emits English first, giving it the oldest `created_at` in the
batch. With the store capped at 8 events across all d-tags, the English
event was always the 9th-newest and got evicted — English readers saw no
news. The catch-up/subscription filter also capped fetches at 4, so fresh
installs only ever received the newest four events.
Raise NEWS_CAP from 8 to 18 (two full nine-language posts) and bump the
news filter limit from 4 to 18 to match. Selection logic is unchanged.
Extend the reconcile_news test with a nine-variant batch that proves the
untagged English event survives under the real cap.
Anonymous-mode censored avatar now composites the Grin mark (the same path,
90% scale, and 67%-black ink a normal gradient avatar draws) over the flat
#FED60E tile instead of the Goblin head. The user's own top-right avatar on
the Home, Pay, and Activity surfaces always renders as that same flat yellow
+ Grin-mark tile (new avatar_self), never a picture or per-identity gradient;
Activity gains the settings avatar to match Home and Pay.
Export archive gains a caption describing exactly what it copies (contacts,
payment history, and requests, to the clipboard as JSON), localized across all
nine locales. The standalone Archive section is removed from the main settings
page; Export archive and Wipe payment history now live inside Advanced privacy.
Restore the Pay tab header top-left rendering to the original goblin
mark (goblin-logo2.svg tinted to the header ink at 40px), reverting the
GoblinPay badge/wordmark theme-split lockup introduced by f768907 (UI
pack 153, merged via build154). Matches the owner reference: the plain
black goblin girlhead silhouette on the yellow header, no badge, no
wordmark.
The two badge assets added by that commit are no longer referenced
anywhere after the revert, so remove them:
img/goblinpay-badge-black.svg
img/goblinpay-wordmark.svg
Scope is strictly the Pay header hunk; the Home wordmark sizing from the
same UI pack is untouched.
Full read-through of the just-landed es and ko locales against en.yml.
ko needed no changes. es had three spots that drifted from its own
established style:
- share_handle / n05_blurb translated "handle" as "identificador(es)"
while every other occurrence in the file (search_hint, no_contacts,
scan_not_recipient, enter_recipient) keeps it as the loanword
"handle" — switched these two to match, consistent with the
fr/de/tr precedent of treating "handle" as an untranslated loanword.
- scan_to_pay_me used the imperative "Escanea" while every sibling
label (scan_to_pay, scan_to_request, tab_scan, scan_qr) uses the
infinitive "Escanear" — aligned it to match.
Full translation of all 918 goblin.* leaf keys from en.yml, byte-matching
key order and %{...} placeholders, standard polite desu/masu register.
House terms follow the es/ko precedent: Goblin/GoblinPay/GRIM/Tor/Nostr/
slatepack/npub/nsec stay untranslated; wallet = ウォレット, recovery
phrase = リカバリーフレーズ, identity = アイデンティティ, relay = リレー,
handle stays as a katakana loanword (ハンドル) throughout.
Font: verified glyph-by-glyph against fonts/noto_sc_reg.otf (already
wired into every font family stack) that it covers every character used
in ja.yml, including hiragana/katakana and kokuji kanji not used in
Chinese, so no new Japanese font file is needed.
Keyboard block keeps literal QWERTY romaji like en/es/fr/de: the custom
on-screen keyboard widget is dead code in this fork (no_soft_keyboard is
always true; native OS keyboards handle all text entry), so there is no
functional need for a kana layout, per zh-CN precedent review.
Registers ja in tests/i18n_keys.rs OTHER_LOCALES and refreshes locale
count/list comments in src/lib.rs, src/gui/views/goblin/mod.rs and
src/gui/views/goblin/data.rs.
The session-open announce that tells the site a trust session exists was
confirmed on the wrong signal and never retried, so the live flow either
toasted announce_failed or returned to the browser without logging in.
Root causes:
- Confirm-on-any-relay: announce_new_sessions marked the grant confirmed
when ANY wallet relay accepted the session-open (out.success non-empty).
The site (magick) subscribes ONLY on its r= hint relay, so an accept on
another wallet default let the wallet "succeed" and return-to-caller
while the site never saw the session. Now confirm requires a relay the
site actually listens on (the hint) via announce_confirmed().
- Announced-set-unconditionally: s.announced was set true even when the
publish failed, and nothing re-armed sessions_dirty, so a failed announce
was never retried and announced_ok stayed empty forever. announced now
flips true only on a real hint-relay confirm; the loop re-arms and
re-publishes each tick until confirmed or ANNOUNCE_MAX_ATTEMPTS, tracked
by a new Session::announce_attempts counter.
- Relay dedup/normalization: channel_relays deduped hints by raw string, so
a trailing-slash or case difference (wss://host/ vs wss://host) forked a
phantom cold relay and broke confirm matching. canonical_relay() now folds
both the union (dedup_relay_union) and the confirm comparison.
- Deadline too short for a cold Tor circuit: TRUST_ANNOUNCE_TIMEOUT_SECS
raised 15s -> 30s to cover reaching a relay we were not already dialed to.
Tests: canonical_relay/dedup, hint-relay confirm semantics, retry cap.
cargo test --lib 246 passed (242 baseline + 4), i18n_keys green, fmt clean.
Owner correction: importing an existing identity belongs at first-run
(onboarding, restored in the previous commit), not in Settings. The
Settings > Identity "Import identity" row was the destructive
replace-the-current-identity action, redundant now that add-identity and
the identity switcher cover bringing in keys non-destructively. Removes
the settings row, the inline ImportState/import_nsec_ui flow, and the
five now-unused settings keys (import_identity, import_identity_title,
import_blurb, identity_replaced, now_using) across all eight locales.
Wallet::import_nostr_identity stays: the onboarding import panel calls
it. The shared settings.* strings the onboarding panel reuses
(importing, import_failed, import_nsec_hint, backup_read_failed,
choose_backup_file, backup_password_hint, import_btn, cancel, close)
also stay. Drift + call-site tests green.
Rotation just does Keys::generate (IdentitySource::Random) — the old
seed-derived path was dropped in Build 8 — so it is functionally a random
add-identity-and-switch, and it does NOT carry the username or profile
over (it RELEASES the old name and hands the new key a nameless identity).
It therefore adds nothing over the identity switcher, so remove it: the
Settings entry row, the inline rotate flow (RotateState + rotate_ui) and
the now-unreachable Wallet::rotate_nostr_identity. Shared helpers
(unlock_all_identities, create_random, nip05::unregister) are untouched.
Drops the fifteen unused goblin.settings.rotate_* / key_rotated /
new_npub / backup_new_key / copy_new_nsec / rotation_failed keys from all
eight locales; drift + call-site tests green.
Onboarding import replaces the just-created identity and is risky, so the
owner ruled it out of the first-run flow; returning users import via the
normal identity-management flow in Settings instead. Removes both entry
points added earlier (the wallet-step import chip and the identity-step
centered button) and the now-unreachable onboarding import sub-flow
(OnbImport state + import_ui), leaving the Settings import path fully
intact. Drops the unused goblin.onboarding.wallet.import_identity{,_hint}
and goblin.onboarding.identity.import_{existing,title,blurb} keys from all
eight locales; drift + call-site tests green.
Replace the long name-authority blurb on the Username page with one brief
line, plus a "Learn more" link that opens the name-authority docs chapter
(https://docs.goblin.st/features/name-authority.html) via the same
open_url idiom used elsewhere in Settings. Authority list, free-type and
save behaviour are unchanged. New goblin.username.learn_more key and the
shortened authority_blurb across all eight locales; drift + call-site
tests green.
While anonymous mode is on, replace every counterparty avatar on the
wallet home and activity panels with one uniform censored tile: a solid
Goblin-yellow (#FED60E) circle with the Goblin mark inked dark on top
(reusing the img/goblin-logo2 asset that widgets_logo draws). Identical
for every identity, so no picture, gradient, or initial leaks.
The Recent strip is now anonymized like the rest of the surface: the
censored tile for every avatar and dotted names. Tap-to-reveal is intact
(the row/tile still senses clicks and opens the full detail).
Shared CENSOR_NAME_DOTS backs the activity title and the Recent names.
Adds a unit test that the censored name is dots-only.
The release build type now applies signingConfigs.release when
android/keystore.properties exists, and stays on the debug-signed
fallback when it is absent, so existing local and CI builds are
unchanged. Also ignore *.jks so signing keys can never be committed.
Translates every en.yml key into es.yml and ko.yml (918 keys each,
placeholder-parity verified), wires both into the drift/call-site test
(tests/i18n_keys.rs), and bundles a Hangul font subset (noto_kr_reg.otf,
~1.9MB, Noto Sans CJK KR Regular subset to Hangul + Jamo + Latin/CJK
punctuation) as a proportional/monospace fallback so Korean UI text
actually renders instead of showing tofu boxes. The Chinese font already
covers Han but not Hangul, which is a disjoint Unicode block.
Language registration itself needed no code changes: rust_i18n::i18n!
auto-discovers yml files in locales/, the locale-detection fallback in
setup_i18n already matches on bare subtags generically, and the language
picker iterates rust_i18n::available_locales!() and renders each
locale's own lang_name key (Espanol, and Hangul for Korean).
Swap the two panels on the Username settings page so the flow reads
top-to-bottom: pick your name authority first (known list + custom
save, default goblin.st), then claim or release your name on it.
Pure reorder of username_ui; no behavior or copy changes.