Public name is now Tane (種, "seed"); Tanemaki (種まき, "to scatter seeds")
is kept only as etymology in the About screen and onboarding.
App not yet distributed, so format tokens rename cleanly with no migration:
backup extension .tanemaki→.tane, QR scheme tanemaki://→tane://, file
prefixes tane-*. Website link and species-catalog User-Agent →
tane.comunes.org. Android label capitalised to "Tane".
Identity/crypto constants already use org.comunes.tane (derivation, HKDF
backup, sync namespace, TANE1 recovery tag) — left untouched, so keys,
backups and sync are unaffected. Tests updated; analyze + full suite green.
Six reported UX issues:
- Inventory 'clear filters' is now pinned outside the horizontal scroll,
always visible — you no longer have to scroll past every chip to find it.
- Empty result now reads 'No seeds match your filters' (not 'no seeds
yet') when a SEARCH or the 'this month' filter hid everything — the
'filtered' flag was missing query + sowThisMonthOnly. Guarded by a test.
- Profile avatar saves + publishes the instant you pick it, so it no
longer looks saved when it wasn't (the Save button sits below the fold).
- Market offer detail shows the author's real avatar (photo/illustration)
instead of a generic person icon — resolves + caches their kind:0 picture.
- 'Wishlist' renamed to Favourites/Favoritos (en/es/pt/ast).
- The drawer 'following' item now opens the web-of-trust 'your people'
screen when the social layer is on (was a dead item).
analyze clean; inventory/market/home + overflow suites green.
Profiles had only a coloured-initial disc. Now each person can set an
avatar — a real photo OR one of our own seed illustrations (so
pseudonymity stays the default; no photo required).
- ui/avatar.dart: the one-string value scheme carried in the kind:0
'picture' — a 'data:' photo thumbnail, a 'tane:seed:<glyph>' token, or
empty. The Nostr ProfileTransport already published 'picture'.
- Photos ride inline as a tiny thumbnail (reuses offerThumbnailDataUri,
24 KB cap) — no media server, like offer photos.
- avatar_edit.dart: pick/take a photo, choose a seed illustration, or
remove; ProfileStore stores it; ProfileScreen shows a big editable
avatar and publishes it.
- PeerAvatar renders photo / illustration / initial fallback.
- ProfileCache gains picture/setPicture; the inbox caches peers' avatars
alongside their names.
- i18n avatar block (en/es/pt/ast). Tests: value scheme + PeerAvatar
render modes, store + cache round-trips.
Follow-up: render peers' avatars in chat/market/your-people (thread the
cached picture into PeerAvatar at each call site).
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.
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.
- 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.
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.
'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.
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
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.
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.
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).
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.
- 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.
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.
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.
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.
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).
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.
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.
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.