Commit graph

192 commits

Author SHA1 Message Date
6898e385e8 polish(detail): TODO review — calendar intro, drop abundance, collapse seed-saving
From the owner's TODO.org review of the shipped features:
- Crop calendar now says the months are YOUR notes: a hint line in the
  editor (cropCalendar.editorHint) and a caption under the 'this month'
  screen (calendar.selfNote) — it read as if the app knew when to sow.
- Dropped the 'how much I have' (abundance) input + display: it competed
  with the lot quantity as a second 'how much'. Removed from the lot sheet
  and the lot summary; the DB column + backup/CSV/catalog plumbing stay
  dormant (no destructive migration). offer_status carries 'do I share'.
- Seed-saving guidance moved into a collapsed expander (detail.seedSaving)
  — reference facts about the species shouldn't sit open among the
  grower's own records; opt-in depth, per progressive disclosure.
- Condition checks already live behind the collapsed 'advanced' expander
  (seed lots only) — left as the discreet power-user surface.

Tests: dropped the obsolete abundance→share nudge test; the seed-saving
section test now expands before asserting. analyze clean; suites green.
2026-07-11 14:08:10 +02:00
00db9d4b6a Merge remote-tracking branch 'origin/main'
# Conflicts:
#	apps/app_seeds/lib/i18n/strings.g.dart
#	apps/app_seeds/lib/state/inventory_cubit.dart
2026-07-11 13:31:04 +02:00
15511ee761 Merge branch 'claude/festive-einstein-384c45'
Ego-centric trust replaces the global Duniter WoT, plus Wallapop-style
ratings v1 (kind 30778). Conflicts resolved: pubspec assets (kept
seed_saving, dropped trust referents), chat_screen (avatars/no-links
from main + rating strip and simplified TrustCubit from the branch),
strings.g.dart regenerated from merged sources.
2026-07-11 13:25:51 +02:00
ee2fdc1e93 docs(trust): record the ego-centric pivot and ratings v1
network-trust.md §2 rewritten (ego-centric model, misuse resistance,
honest losses, unburnt Duniter bridge) + new §2.1 for ratings.
open-decisions.md: 2026-07-10 WoT-parameters decision superseded;
two new dated entries (ego-centric trust, ratings v1 with its soft
conversation anchor and the strong-anchor question kept open).
CLAUDE.md Block 2 paragraph updated to match.
2026-07-11 13:17:32 +02:00
1926e3bd98 feat(ratings): Wallapop-style ratings in chat and offer detail
- SocialSession grows a ratings transport (fifth interface on the one
  shared channel).
- PeerRatingCubit: count, locale-aware average, and circleCount — how
  many ratings come from your own circle (reuses the ego-centric trust
  rule), the signal that makes strangers' review-stuffing irrelevant.
- Chat: slim rating strip under the trust banner; you can rate only
  someone you've talked with (soft anchor until the signed bilateral
  exchange form lands). Sheet with 5 stars + optional comment,
  pre-filled for editing, with a take-back action.
- Offer detail: author's stars under their name, with 'N from people
  you know' when your circle rated them; nothing shown when unrated.
- i18n ratings.* (en/es/pt/ast); tests for cubit and sheet.
2026-07-11 13:13:43 +02:00
acdacf1821 feat(core): person-to-person ratings transport (kind 30778)
Wallapop-style reputation primitive next to trust: Rating (1..5 stars +
optional comment), RatingTransport (rate/retract/ratingsOf/myRatingOf)
and a Nostr implementation on the shared channel. Addressable kind
30778, d = subject: one live rating per (rater, subject) — re-rating
replaces (a single author can never pile up reviews) and retracting
overwrites in place. Client-side dedupe mirrors NIP-01 replacement
order for relays that keep history.
2026-07-11 13:06:53 +02:00
dc7b2eee27 refactor(trust): ego-centric trust replaces the global Duniter membership
The global membership rule (curated bootstrap referents + sigQty/stepMax
parameters) solved sybil-proof identity for a UBI — a problem this app
doesn't have — and its screen leaked graph jargon (npub roots, parameter
steppers). Trust is now computed from the user's own position only:
you vouch / vouched by people you know (distance <=2) / vouched by N.

- TrustCubit: drop networkMember tier, referents and params; keep the
  circle rule and the 365-day vouch expiry (renewable, self-pruning).
- Delete TrustReferents, WotSettings, TrustNetworkScreen and the bundled
  referents asset; unwire injector/bootstrap/app/chat.
- New 'Your people' screen (/your-people, from the profile): who you
  vouch for (revocable) and who vouches for you, names via ProfileCache.
- i18n: wot.* removed, yourPeople.* added (en/es/pt/ast); trust.member
  removed. Kind 30777 events on relays stay fully compatible — only the
  interpretation changes.
2026-07-11 13:03:20 +02:00
332f52f470 Merge branch 'claude/zealous-nobel-7dabeb' 2026-07-11 08:08:39 +02:00
b1fd4c38b5 polish(ux): drop the checkmark on selected filter chips
The tick overlapped the leading icon on selected chips, reading as a
confusing blob. The solid fill already signals 'on', so hide the
checkmark. Also make the selected label reliably white via an explicit
per-chip style: the global ChipTheme's secondaryLabelStyle was not
applied to FilterChips, leaving dark text on green. Plain attribute/
market chips now share a PlainFilterChip widget; swatch and month chips
just drop their tick.
2026-07-11 08:05:57 +02:00
269fb59070 feat(handover): one hand-over sheet replaces the separate sale/plantare doors
'Seeds changed hands' is now the single entry point on the variety
detail (and per lot row): direction (gave/received), one-tap 'all of it'
(default — the whole-plant/shrub case), optional partial quantity,
counterparty, and reveal-on-tap money and return-promise sections that
spawn the Sale/Plantare rows through recordHandover in one save.

Zero-lot varieties still work (ledger rows only); several lots get a
chip picker. The Sales/Plantares screens keep their own add buttons for
off-app records — the sheets stay, demoted to secondary.

i18n handover block in en/es/pt/ast; widget tests for the flow.
2026-07-11 08:00:45 +02:00
665e3e9452 feat(handover): recordHandover — one transaction for the moment seeds change hands
Returns to the original model (data-model §2.8: a closed deal produces a
Movement and, optionally, a Plantare): the given/received Movement plus,
when they came with it, the Sale (informational payment note) and the
Plantare (return promise), all sharing the counterparty. The Movement
carries the promise via the plantare_id column the schema had reserved.

'Gave all' (one tap, the natural path for shrubs/whole plants) moves the
lot's whole quantity, leaves an empty jar (0, same unit), clears
abundance, and returns the lot to private — withdrawing its market offer
on the next publish cycle, price cleared with it.

New enum HandoverDirection; tests in test/data/handover_test.dart.
2026-07-11 08:00:08 +02:00
cfa1053842 feat(offers): asking price on lots, published with sale offers (schema v11)
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
2026-07-11 08:00:08 +02:00
89addb1ed7 feat(seed-saving): source attribution + advisory note
Record where the guidance comes from (copyright/attribution) and make
clear it is general, not local gospel — kept light:
- seed_saving.json gains a top-level sources list (Seed to Seed / Seed
  Savers Exchange / Organic Seed Alliance); parsed into
  SeedSavingData.sources, exposed via SeedSavingCatalog.sources.
- The detail section shows a small muted footer: an italic 'advisory —
  adapt to your climate' line plus a 'Source: …' credit.
- i18n seedSaving.advisory + sourcePrefix (en/es/pt/ast).
- Tests: asset carries sources; the section renders advisory + credit.
2026-07-11 07:52:48 +02:00
d897d3934c polish(ux): selected filter chips fill solid for clear contrast
The selected state used a pale tonal container (seedPrimaryContainer)
that sat almost on top of the pale canvas, so 'on' barely read. Selected
chips now fill solid — seedGreen for the plain attribute/market chips and
the swatch's own ink for family/form chips — with white label, icon and
checkmark. Idle chips stay light with dark ink, so the on/off jump is
unmistakable. White-on-ink AA is locked by a new contrast test.
2026-07-11 07:48:27 +02:00
05227706fd fix(ux): make the scroll cue visible — edge scrim + chevron
The fade-to-transparent was invisible: the row sits on the same pale
canvas, so masking the edge to transparent just revealed more of the
same colour (and often landed on an inter-chip gap). Replace it with a
surface-coloured scrim the chips slide under, topped by an outward
chevron, shown only on an edge with more content. RTL-mirrored.
2026-07-11 07:25:36 +02:00
94cf2f5448 test(seed-saving): drop unused material import 2026-07-11 07:20:37 +02:00
7c2e3f207f feat(seed-saving): per-crop 'how to save this seed' guidance
A bundled, curated knowledge layer — the differentiator of a serious
seed app. For a variety's crop it shows how the plant reproduces and what
it takes to keep the variety true: life cycle, pollination (self/cross +
insect/wind), isolation distance, how many plants to save from, dry/wet
processing, a difficulty pill, and a short tip — all in human words.

- assets/catalog/seed_saving.json: curated public-domain data (Seed to
  Seed / Seed Savers), keyed by botanical FAMILY (defaults) with
  per-species/genus OVERRIDES that win — so e.g. Vicia faba is correctly
  insect-cross-pollinated despite the selfing Fabaceae default, and maize
  is wind-pollinated with a large population. Notes are locale-keyed
  (es/en), extensible to any language — a starter set, not a ceiling.
- domain/seed_saving.dart (pure): enums + SeedSavingGuide (merge, note
  fallback) + SeedSavingData.guideFor (species → genus → family).
- data/seed_saving_catalog.dart: loads the asset once into an in-memory
  singleton (no DB table, no migration); injector loads it at startup.
- VarietyDetail gains  (from the linked species — the reliable
  key, vs the editable category); detail screen shows a _SeedSavingView
  when a guide resolves.
- i18n seedSaving block (labels + enum values + params) in en/es/pt/ast.
- Tests: domain lookup/merge, the real asset's key corrections, and the
  detail section shows/hides. 314 suite tests green; analyze clean.
2026-07-11 07:18:39 +02:00
bfff95fe8d feat(ux): edge fades hint that filter rows scroll horizontally
Chips clipped at the viewport edge with no cue that more filters exist.
New reusable EdgeFade widget (ShaderMask + dstIn) fades an edge only
while content remains beyond it, driven by scroll metrics; RTL mirrors
via Directionality. Applied to the inventory filter bar, market filter
bar and calendar month strip.
2026-07-11 07:15:45 +02:00
1ab243f29e feat(chat): disallow links in messages
Local-first chat between people whose web of trust is still forming is a
natural phishing/scam vector, so messages may not contain URLs. A
conservative `containsUrl` rule (explicit scheme, www., or a common-TLD
domain — but not "3.5kg" or "12.30") gates it: the composer warns and
keeps the text for editing, and MessagesCubit.send drops any URL as a
backstop. Incoming text was already non-tappable (plain selectable text).
2026-07-11 07:14:58 +02:00
6a6a81e8a2 feat(chat): lighter bubbles, my own name on my avatar
Feedback: my bubbles were too dark, and avatars showed only a person
glyph, never the profile name. Now both bubbles are light with dark text
(mine a tonal green, the peer's white with a hairline) — the side and the
per-person avatar colour carry the "who", so nothing is heavy to read.
Load my own display name from ProfileStore so my avatar shows my initial
too, not just the peer's; each avatar still falls back to a glyph when no
name is known.
2026-07-11 07:08:54 +02:00
dc40640682 feat(chat): sender avatar on both sides, coloured per person
Feedback: avatars only on the left, all the same colour, so it was hard
to tell who said what. In a 1:1 only the peer had an avatar (one colour).
Now each message carries its sender's avatar on their own side — mine
trailing, the peer's leading — each tinted by a deterministic colour from
their pubkey. Two sides, two colours: who-said-what is obvious at a glance.
2026-07-11 06:59:24 +02:00
4af90876f4 fix(drawer): scroll destinations, pin the utility footer
The drawer was a single Column with a Spacer pinning intro/Settings/About
to the bottom. Once Calendar was added it overflowed a short screen (~94px),
so Settings and About fell off-screen and their navigation tests couldn't
tap them. Put the destinations in a scrolling ListView and keep the header
and the utility footer pinned, so Settings/About stay reachable at any
height. Fixes the about_screen/home_screen drawer navigation tests.
2026-07-11 06:52:05 +02:00
62ed4f79b3 polish(ux): AA-contrast chip inks, 14-tone palette, calendar month auto-scroll
Blind-build polish pass over the recent sales/chips/calendar work:

- Chip label contrast: 7 palette inks (terracotta, ochre, rose, seedling,
  plant, bulb, cutting) sat below WCAG AA on their own fill; darkened
  (hue preserved) to ≥4.8:1. New category_palette_contrast_test locks it.
- Palette expanded 8 → 14 earthy tones (hues spread around the wheel) so
  family colours rarely collide for a typical 6–10 family inventory,
  keeping the stable-by-name, cross-screen-consistent scheme. Every new
  ink meets AA on its fill.
- Calendar month strip now actually scrolls the selected month into view
  (the comment promised it but it was never wired) — late-year months are
  no longer left off-screen when the screen opens.
- Minor: dead code removed from the month chip.

dart analyze clean; palette/contrast/calendar/quantity/overflow/inventory
suites green.
2026-07-11 06:49:00 +02:00
a476f17cdf feat(home): drop calendar card — drawer entry is enough
Remove the "this month" card from the home menu; the calendar stays
reachable from the drawer and the /calendar route is unchanged. Also
drop the now-unused calendar.homeSubtitle i18n key (en/es/pt/ast) and
regenerate slang.
2026-07-11 06:48:41 +02:00
6cff0d0b11 feat(chat): usable 1:1 chat — bottom-anchored, Drift-backed, dated
- Anchor the message list to the bottom (`reverse: true`) so new messages
  stay in view instead of landing below the fold.
- Move chat history from the OS keystore (O(n²) JSON blob, silent 200-msg
  cap) to a separate encrypted Drift/SQLCipher DB (`ChatDatabase`):
  indexed append, uncapped history, dedup as a unique-key invariant. It's
  an ephemeral per-device cache, isolated from the inventory schema, its
  migrations, and its sync. No data migration (pre-release).
- Add day separators (Today/Yesterday/locale date) and a per-bubble time,
  all via ICU (12/24h per locale; Localizations locale maps Asturian →
  Spanish for intl date symbols).
- Add peer avatars (deterministic colour from the pubkey + name initial),
  surface send failures that were previously silent, and make bubble text
  selectable (addresses, links).
- New i18n keys in en/es/pt/ast; tests for grouping, formatting, avatars,
  scroll anchoring, storage and send errors.

Docs: docs/design/chat-storage.md + open-decisions.md.
2026-07-11 06:40:50 +02:00
171daabce3 feat(calendar): "what's due this month" — dedicated screen + inventory filter
Reuses the existing per-variety crop-calendar masks (sow/transplant/
flowering/fruiting/seed-harvest) — no schema change, no migration — to
answer "qué toca este mes" across the whole inventory. Since the months
are the grower's own record, there's no hemisphere assumption.

- Repo: CalendarEntry + watchCalendar() (varieties with any recorded
  phase); VarietyListItem now carries sowMonths.
- Dedicated CalendarScreen (/calendar): a month strip (defaults to the
  current month, intl month names) and varieties grouped by phase —
  actions (sow/transplant/harvest seed) above informational (flowering/
  fruiting), each tinted, tap → variety detail; empty-month state.
- Inventory filter: a 'this month' chip (shown only when some variety
  has a calendar) keeps what sows in the current month; folded into the
  attributes group + clear-filters.
- Entry points: a live Home card + a drawer item.
- i18n calendar block + menu.calendar (en/es/pt/ast).
- Tests: calendar_test (watchCalendar, list carries sow-months, pure
  filter), calendar_screen_test (phase grouping, month switch, empty),
  plus the screen added to the small-screen overflow guard.
2026-07-11 06:27:57 +02:00
28e8318026 feat(ux): usable amounts, colour-coded category/form chips, currency quick-picks
Four usability passes across the everyday flows:

- Quantity picker: a countable unit (cob/pod/ear…) now defaults to 1 and
  the stepper clamps at a minimum of 1 — you can never store 'a cob' with
  a blank/zero figure. Vibe units (a few) still carry no number.
- Category & form chips are colour-coded (new category_palette.dart): a
  small muted earthy palette maps each botanical family to a STABLE
  tonality (fill + readable ink), and each lot form gets its own tone.
  Applied to the inventory filter bar (now grouped attributes | forms |
  families, separated by a hairline), the list's category headers (a
  matching colour dot), and the lot-form selector.
- Sale currency: quick-pick chips (€ · Ğ1 · hours) fill the field in one
  tap — Ğ1 offered quietly among familiar options, never imposed — while
  free text still takes anything else. New i18n key sale.hours (en/es/pt/ast).

Tests: quantity_picker_test (default-1, clamp, vibe, clear-still-1),
category_palette_test (stable/distinct swatches); overflow guard + the
inventory widget tests stay green.
2026-07-11 06:06:04 +02:00
6de039d518 feat(sales): local sales/purchase ledger (any currency)
A sale is a distinct model from a gift or a Plantare (reproduction
commitment): a recorded seed sale or purchase with an optional price in
ANY currency — €, Ğ1, time, or none yet. Mirrors the Plantare feature.

- schema v10: Sales table (SyncColumns), guarded createTable migration,
  schema dump + generated schema_v10 for the migration round-trip test
- enum SaleDirection { iSold, iBought }
- VarietyRepository: create/watch/watchForVariety/delete + backup
  export/exportForSync/import (LWW-by-HLC, tombstones)
- InventorySnapshot.sales + JSON codec encode/decode (round-trips amount,
  currency, counterparty)
- UI: SalesScreen (/sales), sale sheet, drawer entry, variety-detail
  action beside 'add Plantare'; money hides a trailing .0
- i18n sale block + menu.sales in en/es/pt/ast
- tests: sales repo test (5) incl. Ğ1/price-less/backup round-trip;
  Sales screen added to the small-screen overflow guard

Also harden the overflow guard: swallow the headless engine's image
resource service PNG-decode errors (unrelated to layout) while still
failing on real RenderFlex overflows, so home/about stop reporting
false failures.
2026-07-11 02:33:42 +02:00
de6938d5d7 feat(plantare): usable UI — commitments screen, add sheet, entry points
The reproduction-commitment (Plantare) UI, local-first and plain-spoken:
- A Plantares screen (drawer entry, /plantares) listing your commitments,
  grouped Open / Done, with a big 'mark returned', a menu (let it go / reopen
  / remove) and an empty-state that explains what a Plantare is.
- An add sheet (scrollable): a two-choice 'who reproduces & returns' (I return
  / owed to me), an optional 'with whom', an optional 'what comes back' in the
  grower's own words, and a note. No sale, no currency — a promise.
- Reachable from a seed too: an 'Add a commitment' action on the variety
  detail, pre-attached to that variety.
- i18n en/es/pt/ast (human words: 'Plantare' + a plain explanation).

Added the Plantares screen to the small-phone overflow guard (18 cases green).
2026-07-11 02:11:57 +02:00
81094f25a8 feat(plantare): reproduction-commitment data layer (schema v9)
The Plantare — a promise to reproduce seed and return some (data-model §2.7,
the seed-domain Pledge with return_kind=similar). NOT a sale. Local-first
v1: your honest ledger of commitments (from/to a person by name); the
bilateral signed cross-party form is a later social-layer phase.

- New Plantares table (SyncColumns): varietyId, direction (iReturn/owedToMe),
  counterparty, owedDescription, madeOn, dueBy, status (open/returned/
  forgiven), settledOn, note. schemaVersion 8 -> 9 with a guarded createTable
  migration; schema exported + migration-test helper regenerated.
- VarietyRepository: createPlantare / watchPlantares / watchPlantaresForVariety
  / setPlantareStatus / deletePlantare (soft-delete, CRDT-stamped).
- Included in backups + sync: InventorySnapshot + JSON codec (round-trips
  isDeleted) + exportInventory/exportForSync/importInventory, so commitments
  survive restore and replicate LWW like every other row.

Tests: v1..v8 -> v9 migration + fresh v9; repo create/list/settle/reopen/
delete; and a backup round-trip preserving a commitment. 13 green.

UI (detail section + Plantares screen) follows.
2026-07-11 02:01:25 +02:00
0e41293de5 fix(ui): scroll the germination & condition-check sheets; extend overflow guard
Two save-button sheets wrapped their body in a bare Column (no scroll): the
germination sheet (which grows a list of past tests) and the condition-check
sheet. On a small phone — especially with the keyboard up — the Save button
was pushed off the bottom, unreachable. Both now wrap the body in a
SingleChildScrollView, like the other input sheets already do.

Also extends the small-phone overflow guard to the profile (identity card +
Save), about, intro carousel, and the market/chat screens in their offline
'set up sharing' state. 17 cases green at 320x568 across es/pt/ast.
2026-07-11 01:11:12 +02:00
76556f782b test(ui): extend the small-phone overflow guard (about, intro, profile)
Adds about, intro and the profile screen (identity card + Save button) to
the 320x568 overflow guard across es/pt/ast. All pass — these screens
already fit small phones; the guard now protects them from regressions.
15 cases green.
2026-07-11 00:59:57 +02:00
8315d20f73 test(ui): small-phone overflow guard; fix Home clipping on small screens
Adds test/ui/small_screen_overflow_test.dart: pumps the full screens at a
320x568 (iPhone-SE-class) viewport in the longest-text locales (es/pt/ast);
a RenderFlex overflow fails the test, so a clipped title or save button is
caught. It found — and this fixes — a real bug: the Home menu (a fixed,
non-scrolling Center>Column) clipped ~80px on small phones in every locale.
Home is now wrapped in LayoutBuilder + SingleChildScrollView +
ConstrainedBox(minHeight: maxHeight): centered when it fits, scrolls when it
doesn't, never clipped.

Also makes the test harness faithful: wrapScreen/wrapDetail now map the
locale through materialLocaleFor (ast->es) exactly like the app, so Asturian
screens don't throw on MaterialLocalizations (AppBar tooltips) — that had
masqueraded as an overflow.

Scope notes in the test: full screens only (modal bottom sheets get an
unbounded height in widget tests -> false huge overflow; they already scroll
their body), and the variety-detail screen is skipped (its ~2px hit is on a
DISPOSED/DEFUNCT RenderFlex — a transient during the cubit rebuild, not the
stable layout).
2026-07-11 00:53:44 +02:00
70fb7a463c test: global 90s timeout + document the hang-proof test workflow
Diagnosed the recurring 'tests hang' pain by measurement: the logic layer
is fast and reliable (dart test commons_core ~1s/82 tests; flutter test
test/services ~11s/124 tests, green), and only WIDGET tests hang — because
pumpAndSettle() waits for a screen to stop scheduling frames and a live
source (Drift/Nostr stream, periodic Timer, connectivity/plugin) never does,
so it sits at the 10-minute default and the run looks stuck.

- apps/app_seeds/dart_test.yaml sets timeout: 90s so such a test FAILS fast
  and names itself instead of blocking the whole run.
- CLAUDE.md now spells out the split-by-layer workflow (logic via
  dart test/flutter test test/services; widget tests targeted + timed out;
  never pumpAndSettle a live screen).
2026-07-11 00:26:55 +02:00
21e4357970 fix(sync): replicate deletions and keep photos off the wire
Reusing the backup snapshot for sync had two bugs that would bite real use:

1. Deletions didn't propagate. exportInventory() filters isDeleted=false
   (user backups are tombstone-free by design) and the JSON codec dropped
   isDeleted (omitted on encode, hardcoded false on decode) — so deleting a
   variety on one device never reached the others. Now the codec round-trips
   isDeleted (emitted only when true, so backups stay byte-identical and it's
   backward compatible), and a new exportForSync() includes tombstones. The
   importer already merges them LWW-correctly.

2. Photos bloated events. The snapshot embeds photo bytes as base64, so a
   real inventory would blow past relay event-size limits and sync would
   silently fail. exportForSync() omits attachments entirely (photos stay
   device-local; media sync is a separate, deferred concern).

buildSnapshot() now uses exportForSync(); backups keep using exportInventory
(photos in, tombstones out) unchanged.

Tests: a deletion replicates as a tombstone; the sync snapshot carries no
photo bytes while a sealed backup still does.
2026-07-11 00:17:08 +02:00
225880fc64 feat(sync): device-to-device inventory sync, end to end
Wires the encrypted sync transport into a working replication between one
identity's own devices, reusing the CRDT-ready model (HLC + tombstones +
LWW) the data model was built for.

- SocialSession gains a 5th interface, sync, over the shared connection,
  built with a per-install deviceId + the app's inventory namespace.
- DeviceIdStore: a stable random per-install id in the keystore — distinct
  from the seed-derived CRDT node id, which is IDENTICAL across a user's
  devices (so it couldn't tell them apart / they'd clobber each other's
  replaceable record).
- InventorySnapshotIO (implemented by ExportImportService): build/apply the
  inventory as the SAME interchange JSON as backups but UNSEALED — the sync
  transport does the encryption, so it must not double-seal. Apply = the
  existing idempotent LWW importInventory.
- SyncService: on (re)connect publishes this device's snapshot + subscribes
  to the others'; a local edit (debounced, off tableUpdates) republishes;
  incoming snapshots merge idempotently; skips its own echo; a byte check
  skips re-publishing an unchanged snapshot so the ping-pong converges.
  Foreground only, like the inbox.
- DI/Bootstrap/switchSocialAccount wire and lifecycle it per identity.

Tests: DeviceIdStore, ExportImportService snapshot round-trip (unsealed +
idempotent), SyncService.handleRemote (applies others', skips own).

Follow-ups: deltas vs full-snapshot pushes; background sync; conflict UI.
2026-07-10 23:45:40 +02:00
182c334883 feat(sync): encrypted device-to-device sync transport (core foundation)
The most foundational Block-2+ gap: the data model was built CRDT-ready
(HLC, tombstones, LWW, append-only Movement) and the import reconciler
proves the merge — but nothing replicated a device's inventory to your
OTHER devices. Only manual file backup/restore moved data between devices.

This lands the transport foundation in commons_core (seed-agnostic, on the
shared connection — the 'one connection, N interfaces' shape):

- SyncTransport: moves an opaque, encrypted snapshot blob between ONE
  identity's own devices. The core carries bytes + does the crypto; the app
  decides what the bytes are and how to merge them.
- NostrSyncTransport: NIP-78 app-data (kind 30078, addressable/replaceable
  per device via the d-tag), content NIP-44-encrypted TO SELF, discovery
  filtered to your own author key. Each device keeps one replaceable record.

Tests (MiniRelay, pure Dart): a snapshot replicates to the identity's other
device; the relay only ever sees ciphertext; another identity neither
receives nor could decrypt; re-push replaces; two devices keep their own.

Confirms the open-decisions §D.4 guarantee (sync leaks neither the key nor
plaintext). NEXT slice (app): a SyncService that serializes the inventory
(reusing ExportImportService), pushes on mutation, and runs the existing
idempotent importInventory on receive; a stable per-install device id;
wire sync into SocialSession.
2026-07-10 23:16:07 +02:00
004014be3a feat(inventory): auto-recover from a transient stream failure
Instead of showing a retry button on the first failure, silently re-open
the inventory stream with exponential backoff (250ms→4s, 6 attempts,
staying in the loading spinner). The startup DB-not-ready race clears in
well under a second, so the user just sees the spinner briefly and the
list appears — no tap needed. Only after the retry budget is exhausted do
we surface the manual-retry error.

Tests cover auto-recovery, no error flicker during retries, and the
give-up path (via fake_async to fast-forward the backoff).
2026-07-10 23:10:00 +02:00
f45c452615 fix(inventory): recover from a stuck loading spinner
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.
2026-07-10 22:12:24 +02:00
bb4ee2fd89 refactor(social): one shared relay connection per identity
Every feature (offers, messaging, trust, profile, the inbox listener) used
to open its OWN RelayPool via social.openSession — several sockets to the
same relays, more battery, and reconnection logic living only in the inbox
listener.

Add SocialConnection: ONE shared session per identity, lazily connected and
reused by all. It watches connectivity — dropping the session when the
network goes and reconnecting when it returns, announcing each change on a
sessions stream so the inbox listener re-subscribes automatically. Callers
get the session via connection.session() and never close it (the connection
owns its lifecycle); recreated/disposed on an identity switch.

- InboxService now consumes the shared connection (subscribes on its
  sessions stream) instead of owning its own socket + connectivity code.
- createOffersCubit + the chat/market/profile screens use the shared
  connection; dead createMessagesCubit/createTrustCubit factories removed.
- DI registers SocialConnection per identity; Bootstrap starts it (after
  the inbox subscribes); switchSocialAccount disposes + recreates it.

Tests: SocialConnection lifecycle (reuse, concurrent connect, offline drop +
reconnect, unreachable retry) with a fake opener + online stream; inbox test
updated. nostr added as a dev_dependency for the channel fake.
2026-07-10 21:50:34 +02:00
e2b88b4f26 Merge branch 'claude/nostalgic-wright-acfc25': inline offer photos
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
2026-07-10 21:43:35 +02:00
246bfcfccf Merge remote-tracking branch 'origin/main'
# Conflicts:
#	apps/app_seeds/lib/i18n/strings.g.dart
2026-07-10 21:37:44 +02:00
c74373324a fix(android): enable core library desugaring for flutter_local_notifications 2026-07-10 21:30:22 +02:00
9109581be0 feat(market): embed offer photos inline instead of a media server
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.
2026-07-10 21:27:59 +02:00
48db8fa7c8 Merge branch 'claude/awesome-golick-007e7d': message unread badges + OS notifications
# Conflicts:
#	apps/app_seeds/lib/di/injector.dart
#	apps/app_seeds/lib/i18n/ast.i18n.json
#	apps/app_seeds/lib/i18n/en.i18n.json
#	apps/app_seeds/lib/i18n/es.i18n.json
#	apps/app_seeds/lib/i18n/pt.i18n.json
#	apps/app_seeds/lib/i18n/strings.g.dart
#	apps/app_seeds/lib/i18n/strings_ast.g.dart
#	apps/app_seeds/lib/i18n/strings_en.g.dart
#	apps/app_seeds/lib/i18n/strings_es.g.dart
#	apps/app_seeds/lib/i18n/strings_pt.g.dart
#	apps/app_seeds/lib/ui/chat_screen.dart
2026-07-10 21:15:59 +02:00
fa53295439 feat(market): host and publish the cover photo with each offer
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.
2026-07-10 21:12:36 +02:00
5fe0f4540e feat(messages): unread badges + OS notification for private messages
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.
2026-07-10 21:12:00 +02:00
5bc1715637 feat(commons_core): Blossom MediaTransport for offer photos
Add a MediaTransport interface (sibling of Offer/Message/Trust transports)
and a Blossom (BUD-01/02) backend: sign a kind-24242 upload auth with the
same secp256k1 identity, PUT the bytes over HTTP, return the content-
addressed URL. Idempotent by SHA-256; http.Client injected for tests.
2026-07-10 21:04:47 +02:00
4cf53f259f feat(trust): full Duniter web-of-trust membership (params + referents)
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.
2026-07-10 21:02:08 +02:00
0150a7ce02 feat(market): show offer photos in the market list and detail
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.
2026-07-10 20:58:45 +02:00