The data model always meant sale offers to carry an informational price
(data-model §2.8, sharing-model §4.4); with offer state collapsed onto
the Lot, the price lives there. Null amount on a sell lot = 'to be
agreed' — the NIP-99 event simply omits the price tag.
- schema v11: Lots.price_amount/price_currency, guarded migration,
schema dump + generated schema_v11 for the round-trip test
- ShareableLot/VarietyLot carry the price (sell-only in shareableLots);
addLot/updateLot persist it and clear it when the lot leaves sale
- OffersCubit.publishLots forwards it (outbox flush picks it up free)
- lot sheet: price + currency inputs revealed only on 'For sale', with
the €/Ğ1/hours quick-picks extracted to a shared CurrencyQuickPicks
widget (reused by the sale sheet)
- backup JSON round-trips the new columns
- i18n share.price/priceHint in en/es/pt/ast
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.
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.
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.
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.
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.
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.
The testable core of the offers UI slice, transport-agnostic:
- OfferMapper: pure app-sharing-intent (OfferStatus) -> network Offer/OfferType
mapping; keeps price only for sales, refuses to publish private lots.
- OffersCubit: discover(geohash) streams results; publish() reports the verdict;
depends on the OfferTransport interface (fake in tests) and degrades
gracefully offline (null transport = social layer unavailable, never throws).
8 tests green (fake in-memory transport, no relay). Screen/routing/i18n come
next — they need two product decisions (coarse-area capture, relay list).