The inventory stream subscription had no onError handler, so a transient
failure at startup (e.g. the encrypted DB not yet ready) went unhandled
and left loading=true forever — the spinner that only a restart cleared.
Handle stream errors: drop out of loading, surface an error state, and
offer a retry that re-opens the stream. Add a _LoadError view (i18n en/
es/ast/pt) and cover both the failure and the retry-recovers paths.
Market offers carry a small base64 data-URI thumbnail embedded in the Nostr
event (no media server); full photo stays in the encrypted inventory. Blossom
alternative parked on branch parked/offer-photos-blossom.
# Conflicts:
# apps/app_seeds/lib/i18n/strings.g.dart
Replace the Blossom media-server approach with a fully decentralized one:
generate a small (~320px) JPEG thumbnail from the lot's cover photo and
embed it in the offer event as a data: URI, so photos ride the relays with
no server and no IP leak. The full photo stays in the encrypted inventory.
- offer_thumbnail.dart: downscale-until-it-fits data-URI builder + decoder
- market_widgets: render data: URIs from memory, http(s) URLs from network
- offers_cubit: publish a thumbnail (best-effort) in place of an upload
- drop MediaTransport/Blossom from commons_core (http/crypto deps) and the
media-server setting; parked on branch parked/offer-photos-blossom
Relay event-size limits cap the image to a thumbnail; higher-res sharing
would need the parked Blossom path.
Wire the Blossom MediaTransport into publishing: shareable lots carry
their varietyId, publishLots uploads the lot's cover photo and puts the
returned URL on the offer (best-effort — a hosting failure still publishes
the offer, just without a photo). Add a configurable media server (Comunes
default, empty to opt out) to settings and the market advanced config.
Private messages (NIP-17) arrived in the foreground with no signal: no
unread count, no badge, no OS notification. Hook both into the single
InboxService.ingest choke point.
- UnreadService: per-peer "last read" in the keystore (timestamps+pubkey
only, no message text), live count via a changes stream, active-peer
suppression so the open chat never badges or notifies.
- UnreadBadge: reusable, RTL-aware (Flutter Badge) — on the home hamburger,
the drawer "Chat" item, and per-conversation in the messages list.
- Mark-read on chat open (ChatScreen).
- NotificationService over flutter_local_notifications: generic
"New message from <name>" (no message text, for privacy), payload = pubkey,
tap opens /chat/:pubkey. No-op on web/Windows; Android POST_NOTIFICATIONS.
- i18n notifications.newMessageFrom (en/es/pt/ast).
Background/push stays out of scope (foreground-only by design).
Tests: UnreadService, NotificationService (mock plugin), InboxService hooks
(new peer notifies/counts; duplicate, own, and open-chat do not), and a
UnreadBadge widget test. dart analyze clean; commons_core green.
Slice 4 of Block 2. The pure rule was already parameterised; this adds the
policy, cold-start and UI around it.
- commons_core: WotParams (sigQty/stepMax/sigValidity, Duniter defaults) +
WebOfTrust.membersWith; npubToHex helper (NIP-19 decode) for adding roots.
- WotSettings (keystore): the active parameters, configurable — a young
network loosens them and tightens as it grows, as Ğ1 did. Defaults Duniter.
- TrustReferents: the bootstrap 'seeds' membership is measured from — a
bundled asset (empty until real founders are curated, no invented keys)
unioned with referents the user adds by npub/QR. Honest cold-start.
- TrustCubit: computes the full membership verdict against referents+params
alongside the personal circle, and exposes a TrustTier (networkMember >
inYourCircle > vouched > unknown). Certifications issued with the active
validity (they expire and renew, Duniter rule).
- UI: chat trust badge by tier; a 'Network of trust' screen (manage roots +
advanced params) reached from the profile. i18n en/es/pt/ast.
Tests: WotParams/membersWith, npubToHex, TrustReferents, WotSettings, and
TrustCubit tiers/membership. Resolves the WoT-parameters decision
(open-decisions §B). Trust net stays empty/undetermined until seeded — by
design; users bootstrap their own roots.
Render offer.imageUrl (already carried by NIP-99) as a thumbnail on each
market card and a hero image on the offer detail page. Remote images
degrade gracefully: a spinner while loading, a neutral placeholder when
the media server is unreachable. Publishing the photo comes next.
Adds pseudonymous, switchable social identities derived from the SAME root
seed via an account index (NostrKeyDerivation.deriveFromSeed(seed, account)).
HKDF is one-way so accounts are unlinkable to the Ğ1 key; account 0 is the
original identity, byte-for-byte unchanged (no rotation for current users),
and every account regenerates from the single seed — so switching adds
nothing to back up.
- SocialAccountStore: keystore-backed active account + max created.
- Per-identity stores (chats, profile, name cache) namespaced by account
scope (account 0 = legacy keys, no migration) so identities never mix.
- switchSocialAccount() re-derives the identity, re-scopes the stores and
restarts the inbox listener; RestartWidget rebuilds the tree to pick up
the new social singletons. DB/inventory untouched.
- Profile 'Your identities' switcher: list, create, switch (with a note
that messages/contacts are kept separate per identity). i18n en/es/pt/ast.
Tests: account-indexed derivation (legacy 0 unchanged, accounts distinct
yet deterministic, negatives rejected); SocialAccountStore; per-identity
store scope isolation. Resolves the flagged 'change identity' decision
(open-decisions §B).
Known: switching resets navigation to home (full tree rebuild).
The vecino/market view had three gaps:
- Publishing inventory duplicated the listing. discover() appended every
offer the relay streamed, but relays legitimately resend an addressable
NIP-99 event (stored copy + live echo on publish). Merge by
(authorPubkeyHex, id) so each offer appears once; two authors reusing a
lot id stay distinct.
- No product detail. Tapping a card jumped straight to chat. Add a
read-only "product page" (MarketOfferDetailScreen) with title, mode,
category, eco badge, coarse location, price, a "Shared by" seller
section (profile fetched by pubkey) and the message button. Reached via
a new /market/offer route; cards are now fully tappable. Shows only what
the network already carries — no photos/description added to the wire.
- No filters. Add a filter bar (type, category, eco) mirroring inventory,
each chip group shown only when a discovered offer can match it. To make
the eco filter real, publish the organic flag on the wire: Offer.isOrganic
-> NIP-99 'organic' tag -> OfferMapper -> ShareableLot.
Tests: commons_core organic round-trip; app-side dedup, two-author
separation, filter logic, mapper passthrough; detail-screen widget tests.
i18n keys added to en/es/pt/ast and slang regenerated.
main() awaited all of configureDependencies() (open the encrypted DB,
derive the social key, seed/scan the species catalog) before the first
runApp, so nothing painted until init finished — startup jank visible as
skipped frames. Now main() runs runApp immediately with a Bootstrap
widget that shows a native-splash-matching green splash while the heavy
work runs, then swaps in TaneApp. On failure it shows a localized error
with a retry (configureDependencies is idempotent, so retry is safe).
Adds a bootstrap i18n group (en/es/pt/ast) and a widget test for the
splash/error frame.
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.