The idempotency guard used SocialService as its sentinel, but that was
registered mid-cascade, so a run that crashed part-way could leave a
half-built container the next run would reuse (or hit 'already
registered' on) — intermittently falling back to the inventory-only look
with market/chat/profile greyed out.
- Guard on a dedicated _DepsReady marker registered LAST, so the sentinel
means the WHOLE container is wired, never a partial one.
- Reset a half-wired container (crashed prior run) before rebuilding,
instead of reusing it. Safe: main aborts before runApp on a partial run.
- Make the social key derivation non-fatal: on failure the app opens on
inventory (social hidden) rather than blanking. main tolerates an
absent SocialService.
Discovery searched the full 5-char geohash (~2.4 km) with an exact Nostr tag
match, so two devices in adjacent cells never found each other even though
publish emits the full NIP-52 prefix ladder. Search now coarsens the area to a
configurable precision (SocialSettings.searchPrecision, default 4 ~20 km) via
searchPrefix(), with a human-worded 'how far' selector (Very close / Around
here / My region -> 5/4/3) in the market setup sheet.
Also: surface a clear 'couldn't reach the servers' message when no relay
accepts a share (instead of 'shared 0'), and add wss://relay.comunes.org as the
first default relay (reliable community home; public relays are backup).
Tests: settings precision (default/clamp/roundtrip), searchPrefix helper,
neighbour-cell discovery regression (offers_cubit + commons_core nostr
transports), and the range-selector widget test.
Skip the ~1.5 MB catalog parse and the table scan on every launch — the
main startup cost. `seedBundledIfNeeded` re-seeds only when the recorded
version differs from `speciesCatalogVersion`, writing the version after
the seed commits so an interrupted seed retries next launch.
Adds `parseSpeciesCatalogVersion` and a test keeping the constant in sync
with the asset's `version` field.
Adds Asturianu, translated in full (345 strings), plus the wiring to
make a minority language work end to end — the OS language picker is an
unreliable way to reach it, so the in-app choice must be explicit and
remembered.
- Model Asturian as the real `ast` locale (honest, international-by-design),
not a fake `es-AST` region — slang rejects a 3-letter region anyway.
- Flutter ships no Material/Cupertino/intl localizations for `ast`, so map
the framework chrome to Spanish (`materialLocaleFor`); the app's own text
stays Asturian via slang. Fixes an "Invalid locale ast" crash when the
auto-backup date was formatted with intl.
- Persist the picked language in the keystore (LocaleStore) and restore it
at startup so it survives restarts and wins over the device locale.
- Settings: add the Asturianu tile; compare the full AppLocale so `es` and
`ast` aren't both marked selected.
Tests: LocaleStore round-trip, locale switch + Spanish-chrome fallback,
and a regression for the intl date crash under Asturian.
Match the inventory patterns the market was missing:
- Search: OffersState gains a `query` + `visibleOffers` getter and
OffersCubit.search() filters the already-discovered offers locally by
summary (case-insensitive), never re-hitting the transport. The filter
survives a refresh. Rounded search field mirrors inventory's.
- Pull-to-refresh: RefreshIndicator re-runs discovery for the current
area (empty state included, so a swipe re-scans). Text filter kept.
- i18n: market.searchHint / market.noMatches (en/es/pt).
Note: the shared slang output (strings*.g.dart, incl. the generated
Asturian locale) is regenerated wholesale, so it also carries the
in-progress `ast` locale keys already present in the working tree.
Feedback: at times the social items showed greyed ('like phase 1'), only
inventory active. Cause: a hot restart / Android Activity restart re-runs main
while GetIt still holds the singletons; re-registration threw, main aborted
before rebuilding TaneApp with the social layer, leaving the pre-Block-2 tree.
configureDependencies now returns early if already configured, so re-entry
succeeds and TaneApp is always built with the social layer wired.
Feedback: offline, the social layer stops working with no clear reason. A slim
amber strip now shows above every screen when there's no network (connectivity_
plus), so it's obvious WHY sharing is paused — the inventory keeps working.
Wrapped once via the MaterialApp builder; connectivity check is guarded so it
no-ops where the plugin is absent (tests/unsupported platforms). i18n en/es/pt.
Analyzer clean.
User-flagged (on-device feedback): no way to change the social identity today
(derived once from the root seed at startup). Records the two options —
pseudonymous social key (recommended) vs full root-seed reset — and the runtime
re-derivation implications, for a future round.
Feedback: the market sometimes looked 'disabled'. Now the offline/empty states
are distinct and honest:
- No working relay (defaults exist, so it's a reachability issue) → 'Can't reach
the community servers right now' + a Retry.
- Online but no area set → 'Set your area' prompt (you'd otherwise see a bare
'nothing nearby').
- Area set + nothing found → the existing empty state.
i18n en/es/pt; offline tests updated; analyzer clean.
From on-device feedback:
- Area is no longer shown as a raw geohash 'name'. 'Use my location' is the
primary action; a human status line says whether your area is set (coarse,
never exact). The geohash code + the relay servers moved under 'Advanced'
(code field labelled 'a code like sp3e9 — not a place name'), which also
brings back node/relay selection that had been fully hidden.
- Location denial no longer yanks you into system settings — returns quietly
with an actionable inline message.
- Discovery no longer spins forever when nobody's sharing nearby: a timeout
resolves 'searching' to the empty state.
- Home card 'Open market' -> 'Market' + 'Discover and share seeds nearby'
(the word 'open' confused).
i18n en/es/pt; analyzer clean; the use-my-location test asserts the status flip.
The optional Ğ1 (free currency) integration, per g1-integration.md.
- Profile: an optional 'Ğ1 address' field, persisted locally and published in
your NIP-01 kind:0 metadata (separate from the Nostr key).
- commons_core: UserProfile/ProfileTransport carry a 'g1' field (round-trip
tested).
- Chat: when the peer published a Ğ1 address, a 'Pay in Ğ1' app-bar action opens
their wallet (url_launcher) so value moves in the wallet, not the app — copies
the address as a fallback. No payment rails in-app (sharing-model §4.3).
NOTE (vjrj): the wallet deep-link uses the Cesium web wallet as a universal
fallback; swap the URI in chat_screen._payG1 for the Ğ1nkgo/Ğecko scheme you
prefer. Analyzer clean (both packages); profile transport test covers g1.
Adds a QrView (painted from the pure-Dart 'barcode' matrix — already transitive
via pdf, no native plugin) and shows a QR of your npub on the profile identity
card, so you can hand your identity over with a scan. Analyzer clean.
Closes the profile loop — peers now appear by name, not a raw key.
- ProfileCache: keystore-backed cache of peers' published display names (works
offline) + shortPubkey() fallback.
- Chat: the app bar shows the peer's name (cached first, then freshened from
their kind:0 over the session); falls back to a short key.
- Inbox: each conversation shows the cached name, and missing names are resolved
over one session and cached.
- Wired via DI + TaneApp(profileCache).
Analyzer clean; ProfileCache covered by a plain test.
Two UX reports:
- Market/Messages (and Profile/Chat) had no back button — they were opened with
context.go (replaces the stack). Switched the drawer entries, home Market card,
offer 'Message' link and inbox taps to context.push, so the AppBar shows a back
arrow to return.
- The market's upload icon (tooltip-only) was unclear. Replaced it with a labelled
FloatingActionButton.extended 'Share my seeds' (campaign icon); the config gear
stays in the AppBar.
Analyzer clean.
Drawer 'Profile' is now live.
- commons_core: ProfileTransport + UserProfile + NostrProfileTransport (kind:0
publish/fetch), exposed as SocialSession.profile. Round-trip tested against the
in-process relay (fast dart test).
- app: ProfileStore (local name/about, keystore) + ProfileScreen — shows your
shareable npub with copy, edits display name + 'about', saves locally and (when
online) publishes kind:0 so peers can recognise you instead of a raw key.
- Drawer 'Profile' -> /profile (gated like Market/Chat).
- i18n en/es/pt. ProfileStore + profile transport covered by plain/dart tests.
Analyzer clean (both packages).
Chat was only reachable via an offer's 'Message' button, and the drawer 'Chat'
sat on 'coming soon' — so messaging/trust were effectively hidden.
- MessageStore keeps a conversation index (the keystore has no key enumeration)
and exposes conversations() — peers newest-active-first with their last line.
- ChatListScreen: the messages inbox (tap a conversation → /chat/:pubkey).
- Drawer 'Chat' is now a live destination -> /messages (gated like Market).
- i18n en/es/pt. conversations() covered by a plain test (no widget hang).
Analyzer clean.
Messages were in-memory only — gone on leaving the conversation.
- MessageStore: keystore-backed (no plaintext), a capped per-peer JSON list
(last 200). Simple; the encrypted Drift DB is the eventual home at scale.
- MessagesCubit: loads saved history on start (subscribes FIRST, then loads, so
a message arriving during the load isn't dropped) and persists every sent and
received message. Wired via DI + TaneApp(messageStore).
Tests (plain 'test', no hang risk): MessageStore round-trip/per-peer/cap, and a
cubit test that reopens a fresh cubit and sees the saved conversation. Analyzer
clean; run 'flutter test' locally to confirm.
Even with fixed pumps, driving discovery through testWidgets hangs here (the
transient 'searching' spinner never lets pumpAndSettle settle). Discovery logic
is already covered by offers_cubit_test (plain test + pumpEventQueue). Keep only
the offline/setup widget tests, which have no live stream. Reported by the user.
A network-wide 'N people vouch' count invites spam and means little. Now the
trust banner computes whether the peer is within YOUR circle — you vouch for
them, or someone you vouch for does (friend-of-a-friend) — via the pure
WebOfTrust rule (seeds={you}, threshold 1, distance 2). When known, the banner
turns green with 'In your circle' (else it falls back to the count).
The full member policy (threshold/distance/bootstrap) stays open (network-trust
§2); this is a deliberately loose 'people near you' heuristic.
Analyzer clean; trust cubit test adds a friend-of-a-friend vs stranger case
(run flutter test locally to confirm — the widget suite is too slow to run here).
The 'searching' state shows a CircularProgressIndicator, so pumpAndSettle never
settled and the widget test hung ~10 min. Pump a couple of fixed frames instead
(the buffered offers deliver on a microtask), and close the fake discover stream
so it's finite. Reported by the user.
Three feedback fixes:
- Servers are fully hidden now — relays are managed automatically (default
community servers; the pool skips dead ones). Sharing setup shows only 'your
area'; no jargon-y URL field. (3 default relays for redundancy.)
- Drawer 'Market' is a live destination (was stuck on 'coming soon'), gated on
the social layer being wired like the home card.
- My own listings in the market carry a 'You' badge (and no 'message yourself'
button), so it's clear which offers are mine.
Analyzer clean. Note: could not run the flutter widget suite here (10+ min
compile after the geolocator native dep in this sandbox) — run 'flutter test'
locally to confirm; the You-badge test was simplified to be robust.
Feedback: the blank 'web address' field was unusable — nobody knows what to put,
so the market never worked at first launch.
- SocialSettings ships default public relays (nos.lol, relay.damus.io), applied
automatically for new users so the market works day one. An explicitly empty
choice still turns the network off (privacy). Exposure is minimal: offers are
opt-in and carry only a coarse geohash.
- Sharing setup now shows the servers as a short PICK-LIST (checkboxes, pre-
selected) instead of a blank field; a community's own server goes under a small
'add another' escape hatch. Intro reworded — no need to know a community.
- Tests updated (defaults) and pinned offline (setRelayUrls([])) so widget tests
don't hit the network. 22 green, analyzer clean.
Reported: sheet overflowed 7.7px with the keyboard up, and 'use my location'
seemed to do nothing.
- Config sheet is now scrollable (SingleChildScrollView) — no keyboard overflow.
- 'Use my location' shows an inline spinner + inline error (the old snackbar was
hidden BEHIND the bottom sheet, so failures were invisible). Message is now
actionable ('check location is on and the permission is granted').
- Provider hardening: request permission first; on permanent denial open app
settings; if location services are off open location settings; time-limit the
fix and fall back to last-known position so it doesn't hang indoors.
Analyzer clean; market tests green.
- TrustCubit: reads how many vouch for the peer + whether YOU do; toggleVouch
certifies/revokes over TrustTransport; never vouches for self; offline-safe.
Kept simple — the full known-member rule (threshold+distance) and its
bootstrap set are still-open policy (network-trust §2); a live count + your
vouch is the honest first step.
- Chat screen now opens ONE session carrying both messaging AND trust (the
shared-connection shape), and shows a slim vouch banner: 'Vouched for by N' +
an 'I know this person' / 'You vouch for them' toggle.
- i18n en/es/pt (human words: 'I know this person', not 'certify').
Tests: 4 trust cubit + messaging + chat, 10 green; analyzer clean.
- MessagesCubit: 1:1 conversation over MessageTransport. Filters the shared
inbox to the peer; tags our own sends with our pubkey (NIP-17 wraps don't come
back to the sender) so the UI can align bubbles; degrades gracefully offline.
- ChatScreen (mockup 10): message bubbles (mine right / peer left) + a composer;
opens a session lazily, shows 'set up sharing' when there's no relay.
- Entry point: a 'Message' button on each offer card (hidden on your own
listings) → /chat/:pubkey.
- Routing + i18n en/es/pt.
Tests: 5 cubit (send/receive/order/filter/offline) + chat offline + market, green.
Sender-side offline delivery hardening.
- OfferOutbox: a keystore-backed (no plaintext) queue of lot ids to publish
later. Stores ids, not offers, so on flush each offer is rebuilt from the
lot's CURRENT state — an offline edit is respected, a deleted lot drops out.
- flushOutbox(): on reconnect, republishes queued lots and clears them (no-op
offline; drops ids whose lot is gone).
- Market screen: sharing offline parks the lots ('we'll share these when you're
connected'); opening the market online auto-flushes the queue first.
- Wired via DI + TaneApp(outbox); i18n en/es/pt.
Tests: 4 outbox + 3 flush + market UI, 20 green. Analyzer clean for new code.
Relay-strategy hardening (network-trust §3): don't hang on one relay.
- NostrChannel interface: what the transports need (sign, publish, subscribe,
reqOnce). Implemented by NostrConnection (single relay) and the new RelayPool.
- RelayPool: connects to all configured relays (skips unreachable ones, throws
only when none are reachable), publishes to every live relay (accepted if any
accepts), merges discovery deduped by event id, and survives a relay dropping.
- Transports now depend on NostrChannel, not a concrete connection.
- SocialService.openSession takes ALL configured relays (was relays.first);
createOffersCubit passes the whole list.
commons_core 63 tests green (3 new pool tests over two in-process relays);
app_seeds social/market 18 green.
Makes the coarse area human, per feedback that a typed code is jargon.
- CoarseLocationProvider interface + geolocator-backed impl (BSD-3): low
accuracy only, returns null on any denial/error (degrades quietly). Behind an
interface so it's fakeable and the plugin stays at the composition root.
- Config sheet: a 'Use my approximate location' button (shown only when a
provider is wired) fills the area from the device position, reduced to a
5-char geohash via commons_core's Geohash — a precise fix never leaves the
device. 'Couldn't get your location' on failure.
- Threaded via TaneApp(location); main wires GeolocatorCoarseLocation.
- Platform: ACCESS_COARSE_LOCATION (Android) + NSLocationWhenInUseUsageDescription
(iOS), both scoped to the coarse-area use.
- i18n en/es/pt. Widget test fills the area from a fake location. 8 tests green.
The other half of the market: share your marked lots as offers.
- VarietyRepository.shareableLots(): non-private lots WITH their id (stable,
updatable offer id) + the variety label. Private lots never leave the device.
- OffersCubit.publishLots(): maps each via OfferMapper and publishes, tagged
with the coarse area + signed by the derived key; counts acceptances; no-op
offline or with no area.
- Market screen: a 'share my seeds' action (online only) — publishes, then
re-discovers to show them; 'mark some seeds first' when nothing's shared.
- i18n en/es/pt.
14 tests green (repo query, cubit publish, market UI); analyzer clean for new code.
A silent safety net against in-app data loss or DB corruption, with zero
friction. Driven off the app lifecycle (startup + when hidden), it writes a
sealed .tanemaki copy into the app's private storage once a week, keeping the
newest 3 (rotating). Reuses the manual backup's encryption via the extracted
ExportImportService.buildSealedBackup(); never throws, so a failed copy can't
crash startup or backgrounding. Not registered on web (no file storage).
Settings shows a quiet reassurance line with the date of the last automatic
copy (localised via intl), hidden where no automatic backup exists.
Pure Dart lat/lon -> low-precision geohash (public-domain algorithm), the piece
the coarse-area capture needs so people never type a code. Precision-controlled
(5 chars ~ ±2.4 km). Tested against the canonical u4pruydqqvj vector + prefix/
proximity properties. 5 tests green.
The market config leaked network jargon ('community servers / one address per
line'), confusing for the 10-80 audience (violates the human-words rule):
- Lead with a plain intro: what sharing does + a community you trust runs a
shared meeting point, no company in the middle.
- Bury the technical web address under an 'advanced' expander (most people won't
have one; 'leave empty if you don't have one yet').
- Human labels: 'Your area' (not 'Area code'), 'Web address' under advanced.
Market tests green.