Tane dialled its four default relays at launch, before anyone had asked
for anything — an F-Droid reviewer spotted it, and they were right. The
seed book needs no network at all, so the app should not have one until
the person joins the sharing side.
- SocialSettings gains a three-state `sharingEnabled`. `null` means
"never asked", which is what lets `migrateSharingEnabled` keep an
existing install exactly as it was: anyone past the intro was on a
build that connected at launch, so they keep messaging, device sync
and offer alerts. A fresh install starts fully offline.
- bootstrap only starts the shared connection when sharing is on. The
inbox/sync/plantaré/alert listeners are untouched: they react to a
session, and none arrives.
- SharingSwitch is the single place that moves the stored choice, the
live connection and the flag the UI listens to, so they cannot drift.
- Agreeing to the community rules is the opt-in — one consent surface,
reached from the market or from the drawer's invitation.
- SocialConnection.start is now idempotent and gains stop(), so turning
sharing off goes offline immediately instead of at the next launch.
- The social drawer entries stay visible but padlocked while sharing is
off; tapping one explains what wakes up and offers to join. Hiding
them would have kept the tool a secret. "Coming soon" is gone for
good — everything it labelled is built.
Covered by tests for the migration in both directions, start/stop
lifecycle, the gate turning sharing on, the invitation, and the drawer
in all three states (no social layer / off / on).
F-Droid's APK scanner rejected v0.1.14: 6 com.google.android.play.core.*
(SplitInstall/SplitCompat/tasks) references. They came from Flutter's
io.flutter.embedding.engine.deferredcomponents.PlayStoreDeferredComponentManager
being pinned by our over-broad '-keep class io.flutter.** { *; }' ProGuard
rule — Tane uses no deferred components, so R8 should shrink that manager
away (as it does for immich and other Flutter F-Droid apps, which don't
carry such a keep). Narrow the keep to io.flutter.plugins.**; JNI-critical
embedding classes (FlutterJNI, …) stay via the embedding AAR's own consumer
ProGuard rules — verified the built APK still contains FlutterJNI and no
com.google.android.play.core.* at all. Also exclude the com.google.android.play
group at the dependency level (belt-and-suspenders) + -dontwarn.
Bumps to 0.1.15 (+17); fdroid recipe versionCodes 171/172/173.
Flutter auto-adds android.permission.INTERNET only to the debug/profile
manifests, so every RELEASE build (Play + F-Droid) shipped with no network
permission at all — the whole social layer (market, messaging, trust, sync)
was dead on Android release, while desktop (no permission model) worked. The
market's 'can't reach the servers' was the visible symptom. Adds it to
android/app/src/main/AndroidManifest.xml.
Release prep v0.1.14: pubspec 0.1.13+15 -> 0.1.14+16; fdroid recipe
versionCodes 161/162/163, commit v0.1.14, CurrentVersionCode 163.
v0.1.11 and v0.1.12 bumped pubspec without advancing the fdroid recipe, so the
fdroid_reference job computed versionCodes 131/141 that the recipe (still
121/122/123) didn't define — 'fdroid build' found no such versionCode and the
job failed. The Play deploys were unaffected.
- pubspec: 0.1.12+14 -> 0.1.13+15.
- fdroid recipe: Builds versionName 0.1.13, versionCodes 151/152/153,
commit v0.1.13; CurrentVersion 0.1.13, CurrentVersionCode 153.
Play accepts versionCode 15 (new); fdroid_reference now builds 151/152/153.
Bumps 0.1.11+13 -> 0.1.12+14. Ships the market fixes: auto-recovery when the
shared relay connection comes up (no more stuck 'can't reach the servers' on a
fresh install), per-relay connect timeout, zone prompt before the connection
error, un-clipped Save button, and honest 'community servers' copy in app + site.
fdroid recipe intentionally not bumped here (as with v0.1.11); F-Droid is tracked
in its own reproducible-build MR.
The setup intro claimed sharing happened 'with no company in the middle'
while the sheet itself lists community servers. Reworded in all repo-authored
locales (en/es/fr/de/pt/pt_BR/ast): what you offer travels through community
servers run by people and collectives rather than a company.
- A new user's first step (set your area) works offline, so that empty state
now wins over 'can't reach the servers'
- The sharing-setup sheet wraps in SafeArea(top: false) so the Save button
isn't clipped by the system navigation bar on edge-to-edge devices
Fresh installs could sit on 'can't reach the servers' forever: the offers
cubit captured the transport once at build time, the shared connection only
retried on a connectivity CHANGE, and a silently-filtered relay could stall
the pool for minutes.
- SocialConnection: retry with backoff after a failed attempt while started
and not knowingly offline (injectable schedule for tests)
- OffersCubit: follow connection.sessions, re-attach the transport and re-run
the last discovery on (re)connect; announce drops via connectionEpoch
- market _init: record the wanted area on the cubit even while offline
- NostrOfferTransport.discoverPage: sort a copy (channel lists may be
unmodifiable)
- play job: apply the same two fixes the fdroid recipe uses (strip
flutter_tesseract_ocr's dead-jcenter buildscript; cap Gradle heap to
-Xmx4g + no daemon) so a cold-cache production AAB build can't hit
either blocker. play builds the AAB directly, not via the recipe, so
it was still exposed.
- pubspec: 0.1.9+11 -> 0.1.10+12.
- fdroid recipe: versionName 0.1.10, versionCodes 121/122/123,
commit v0.1.10, CurrentVersion(Code) 0.1.10/123. binary: uses %v so
it resolves to the v0.1.10 release assets the tag build will upload.
The v0.1.10 tag is intentionally NOT cut here: cutting it deploys to
Google Play PRODUCTION (100%) and uploads the 3 signed reference APKs.
Fill the ~74 UI strings pt.i18n.json was missing (saved searches, label
scanning, Plantaré, lot history), then add pt_BR as a proper Brazilian
variant rather than a copy of European Portuguese: tu/teu/tua -> você/
seu/sua with matching verb conjugation, partilhar -> compartilhar, and
Guardar -> Salvar for UI actions (seed-saving keeps "guardar", the
correct term in both variants). Wires AppLocale.ptBr into the language
picker and regenerates slang output.
Device compatibility (regression fix):
- The CAMERA permission (from zxing_barcode_scanner / image_picker) implicitly
required android.hardware.camera, excluding camera-less devices — Play showed
Automotive -96%, Chromebook -86%, TV -25%. Declare camera & location
uses-feature required="false" to keep those devices supported.
- Detect a real camera at runtime (PackageManager.FEATURE_CAMERA_ANY via the
existing MethodChannel), cache it at bootstrap, and hide the QR scan button and
the camera photo-source option when absent, so no broken actions are offered.
Play "app optimization" recommendations:
- Enable R8 (isMinifyEnabled + isShrinkResources) with keep rules for the
OCR (tesseract4android), SQLCipher and notifications JNI/reflection code.
- Bitmap downscaling: cacheWidth/cacheHeight (and ResizeImage for avatars) on
list thumbnails and avatars so photos decode to on-screen size, not full res.
- Edge-to-edge: opt in with transparent system bars in main().
Release flow:
- Tagged builds now publish to the production track at 100% (was internal);
add a manual deploy_internal lane as a QA safety net.
- Document country/region availability as a Play Console setting (not in-repo).
Bound market memory and let large areas page instead of loading everything:
- DiscoveryQuery gains an until cursor; OfferTransport gains discoverPage()
(one-shot, EOSE-bounded, newest-first) alongside the existing live discover()
stream, which now accepts since so it only carries NEW offers going forward.
- NostrOfferTransport.discoverPage sorts by created_at desc and derives the
next cursor from the oldest event seen (not the oldest type-matched one, so
filtering never skips a page).
- OffersCubit: discover() fetches the first page + opens a since=now live sub;
loadNextPage() pages further back; the in-memory list is capped at 400
offers (each can carry a ~40KB inline photo thumbnail, so unbounded growth in
a busy area was a real OOM risk).
- market_screen: infinite scroll via a scroll-position trigger + footer
spinner, instead of a single unbounded ListView.
- Added discoverPage unit tests (commons_core) and cubit pagination tests
(first page/cursor, accumulation, cap, cross-page dedup).
Scale the local inventory to 10k+ varieties:
- Render the list with ListView.builder over a flattened header/item model
instead of building every tile upfront.
- Store a small regenerable JPEG thumbnail per photo (schema v14) and use it
for the 48px list avatar; full bytes stay for offer image hosting. Existing
photos are backfilled lazily at startup. Thumbnail is local-only (excluded
from CRDT sync and backups by the JSON codec).
- Add indexes on varieties(is_deleted,is_draft), attachments(parent_type,
parent_id,kind), lots(variety_id) via @TableIndex.
- Debounce watchInventoryView (~250ms) so a burst of table writes triggers one
reload, not seven.
- cacheWidth/cacheHeight on the list avatar decode.
- Scale test raised 3k -> 10k; migration test v13 -> v14.
One passive line in the batch story after a 'sown today' tap — 'this
species crosses, grow it ~400 m from others' — sourced from the bundled
seed-saving guidance (family default + species override). Selfers get
nothing: the trap is recording isolation religiously for beans where it
never mattered. No input fields, no schema change.
Closes the physical↔digital loop the labels opened: the QR on a printed
envelope (tane://seed, already encoded by seed_label_codec) can now be
scanned from the inventory bar. A known label opens its variety; an
unknown one asks before adding anything, then creates the variety (species
linked by exact scientific name when bundled) with a lot carrying the
label's year and origin. Scanner is pure ZXing (zxing_barcode_scanner,
MIT) — no ML Kit, no Play Services, F-Droid-safe, the stack Ğ1nkgo ships.
Mobile-only; other platforms don't show the button (OCR precedent).
The post-scan flow is a plain function (handleScannedPayload) so it's
widget-tested without a camera.
The note growers most wish they had two seasons later — did it do well,
was it worth keeping — never had a home. New append-only GardenOutcomes
table (lotId, season year, three-face rating, free note); the question
appears ONCE, inline, right after recording a harvest in the batch story,
and is skippable without a trace. The answer shows as one more story
line. Rides backups/sync like every mutable table (JSON codec, LWW
import, HLC clock absorb). Migration v12→v13 guarded + verified from
every historical version.
The Movement log (history + provenance DAG) was recorded but invisible.
A new history sheet on every lot tile narrates the batch: movements,
germination tests, storage checks and how it entered the collection
(origin attached), most-recent first. Two one-tap chips record 'sown
today' / 'harvested today' — the first UI door to the sown/harvested
movement types. No schema change; one-shot reads only (no live
subscriptions in a transient sheet).
Adds a versionCodeOverride to build.gradle.kts (F-Droid requires distinct
versionCodes per split APK; Google Play's App Bundle is unaffected since
this only touches the application-variant/APK output). Bumps to 0.1.2+4.
Verified locally: flutter build apk --release --split-per-abi produces
app-{armeabi-v7a,arm64-v8a,x86_64}-release.apk with versionCode 31/32/33.
Save the current market search (query + facets) and get notified when a
matching offer appears in your zone while the app is open, with a catch-up
scan on start. Adds a save affordance in the search bar, a saved-searches
list (apply/delete) with a per-search new-match badge, an AppBar entry
badged with the unseen total, and notification-tap routing to the list.
Alerts evaluate against the user's current area, dedup via seen keys, and
skip own/blocked/hidden/non-active offers. en+es strings.
One discovery subscription over the shared connection for the user's
current zone; matches each incoming offer against saved searches and
fires a text-free OS alert on the new 'alerts' channel for fresh matches.
Skips own/blocked/hidden/non-active offers and dedups via seen keys. The
replay-on-connect doubles as the catch-up scan. Notification payloads are
now prefixed so taps route to chat or the saved search.
Keystore-backed, account-scoped store mirroring SavedOffersStore. Tracks
per-search seen keys (dedup, capped 500) and unread match keys (badge
source), with markMatched/markViewed/newMatchCount and a changes stream.
Extract the market's query/type/category/organic filter chain into
SavedSearch.matchesFilters so live results and saved-search alerts can
never disagree. OffersState.visibleOffers now delegates to it.
Bumps to 0.1.1+3 (versionCode 3). Adds CHANGELOG [0.1.1], fastlane
changelogs (en-US, es-ES) for versionCode 3, and a 0.1.1 build entry in
the F-Droid recipe (commit v0.1.1, CurrentVersion/Code 3).
Removes the geolocator dependency, which transitively embedded
com.google.android.gms.* (play-services-location) — proprietary classes
that F-Droid's scanner rejects. The optional coarse-location button now
talks to Android's own LocationManager over a small platform channel
(org.comunes.tane/coarse_location): permission prompt, single coarse fix
with a 12s timeout, last-known fallback, null on any failure. Behavior
is unchanged and the CoarseLocationProvider interface is preserved, so
the UI, i18n and widget tests need no changes.
Also sets dependenciesInfo.includeInApk/Bundle = false so the AGP
dependency-metadata block (also flagged by F-Droid) is not embedded.
Neither change affects the Google Play build.
Format: repo predates Dart 3.11's tall formatter (~128 files would need a
dedicated reformat commit); disable --set-exit-if-changed for now, keep
flutter analyze as the real gate.
Test: 'drawer Settings opens the settings screen' asserted find.text('Español'),
which only holds when the resolved locale is Spanish. Under AppLocale.en (as
set in the test) the tile reads 'English', so it failed under CI's non-Spanish
host locale. Assert by the language selector's Key + icon instead (locale-robust).
Noto Sans wordmark on brand green (#2F7D34), white sprout, and the official
value tagline ('keep and share traditional seeds' / 'guarda y comparte...').
Generated by tool/gen_feature_graphic.py for en-US and es-ES.
- fastlane: Appfile/Fastfile/Gemfile with deploy_play lane (AAB -> Play internal track)
- CI: tag-gated build:android (signed AAB/APK from CI secrets) + deploy:play jobs
- F-Droid: fdroiddata build recipe at docs/fdroid/org.comunes.tane.yml
- Play compliance: Data Safety / content-rating answer sheet (docs/legal/internal)
- docs/release.md: automated tag-triggered flow, dedicated tane-upload keystore
- pubspec: description now mentions the market, not just the inventory
The Cancel / Save-and-add-another / Save actions were a fixed Row that
could not shrink, so on narrow phones and long locales (es/pt/de/fr) the
buttons overflowed the right edge (the clipped 'Guardar'). Replace it with
an OverflowBar that wraps onto stacked, end-aligned lines when they don't
fit; move the added-count onto its own line above.
Guard it in small_screen_overflow_test.dart across the long locales.
Beta-tester feedback triage. The data model already had LotType.seedling as
first class; the gaps were copy and UX.
- Seedlings/plantones: broaden landing hero (EN/ES), in-app legal notice, and
all legal texts (site + docs masters) from seeds-only to "seeds and
seedlings", with a live-plant phytosanitary/transport caveat. Seeds stay the
hero — targeted, not a blanket rename.
- Backups: settings line now states the 7-day cadence explicitly; cadence lives
in AutoBackupService.backupInterval as the single source for text and schedule.
- Community servers: replace the manual wss:// text box with a checklist of the
known servers (defaults visible/toggleable) plus an "add server" affordance
with basic validation. No jargon in the UI.
i18n across en/es/pt/fr/de/ast (ja falls back). Tests updated + a new
server-picker widget test.
- Hugo site (site/): EN/ES intro + legal (generated from docs/legal), an
ES/EN About page from docs/{que-es-tane,what-is-tane}.md, seed-green theme,
responsive header, store-badge placeholders, multi-stage Hugo->nginx image.
- Golden screenshot harness (test/screenshots/): en,es,fr,de,pt,ja + RTL demo,
real fonts via DejaVu Sans + FontManifest; skip-by-default tag so CI stays
green. collect_screenshots.sh feeds site/ and fastlane phoneScreenshots.
- Drop editorial notes from the public explainer.
The propose sheet used a free-text seed name, unlinked from the
inventory. Now the seed is picked from your catalogued varieties so the
Plantaré is always tied to a real Variety (tap-through to its detail, and
it shows in that seed's commitments):
- Seed field is an Autocomplete over the inventory; on send the typed name
resolves to the matching Variety, or creates a quick one if it's new —
so varietyId is never null. Sheet takes the VarietyRepository; chat
passes it.
- New repo one-shot `varietyLabels()` (id + label, no joins) feeds the
picker. Deliberately a Future, not `watchInventory()` — a transient
sheet must not hold a live Drift subscription (that hung the widget test
~36 min). Tests use it too and now run in ~2s with bounded pumps + a
tall surface, no pumpAndSettle.
Tests: a new name creates & links a Variety; a matching name links the
existing one without duplicating. ui/services/data plantare suites green.
The viral loop, made usable end-to-end:
- Propose sheet (plantare_propose_sheet.dart): from a chat with a peer
(their key already in hand), pick your side, name the seed, choose what
comes back (similar · non-GMO organic / work hours / other) and an
optional return-by, then sign and send. Reached from the chat overflow
menu; confirms "waiting for them to sign".
- Plantares screen: incoming proposals show Accept & sign / Decline right
on the tile; every bilateral row wears a handshake badge — awaiting
signature, signed by both, or declined. Accept falls back to a gentle
"offline" nudge if the proposal isn't in hand yet.
- i18n: full en + es for the bilateral flow; other locales fall back to
base per slang config.
Wire-up only touches human copy (no jargon) and stays behind the
optional PlantareService, so inventory-only builds are unaffected.
Tests: propose sheet drives a real sign+send; the ledger renders the
signed/awaiting badges. Fake SocialSession in your_people test gains the
new transport getter. Widget tests targeted + timed. All green.