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.