From 496328262025bd5f28510d9c737eb5b5e32814ca Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 00:01:20 +0200 Subject: [PATCH 1/8] fix(site): only wrap header nav on small screens if it overflows The forced width:100% on .site-nav below 560px was written back when the language switcher spelled out every language name and needed the room. Now that it's a compact pill, the header fits on one line down to ~360px; let flex-wrap handle the actual overflow case instead of always forcing a second row. --- site/assets/css/main.css | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/site/assets/css/main.css b/site/assets/css/main.css index 9cb1bc9..55e230f 100644 --- a/site/assets/css/main.css +++ b/site/assets/css/main.css @@ -121,11 +121,15 @@ a { color: var(--green); } .lang-menu a.lang:hover { background: var(--container); } .lang-menu span.current { color: var(--muted); font-weight: 600; } -/* Narrow phones: brand on its own row, nav wraps beneath it, left-aligned. */ +/* Narrow phones: shrink a bit, but only wrap the nav below the brand if it + actually stops fitting — the language menu is a compact pill now, not + three spelled-out language names, so it fits alongside "About Legal" down + to fairly small widths. flex-wrap on .site-header handles the overflow + case on its own; no need to force it. */ @media (max-width: 560px) { .site-header { gap: .5rem; } .brand { font-size: 1.15rem; } - .site-nav { width: 100%; gap: .4rem 1rem; font-size: .95rem; } + .site-nav { gap: .4rem 1rem; font-size: .95rem; } } main { max-width: var(--maxw); margin: 0 auto; padding: 0 clamp(1rem, 4vw, 2rem); } From 3ec6ed2329f7ccb744728f8fc7fea83b81865d4d Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 14:26:42 +0200 Subject: [PATCH 2/8] v0.1.11 --- apps/app_seeds/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app_seeds/pubspec.yaml b/apps/app_seeds/pubspec.yaml index 241cb72..98b13c1 100644 --- a/apps/app_seeds/pubspec.yaml +++ b/apps/app_seeds/pubspec.yaml @@ -1,7 +1,7 @@ name: tane description: "Tane — local-first, encrypted, decentralized traditional-seed inventory and market." publish_to: 'none' -version: 0.1.10+12 +version: 0.1.11+13 environment: sdk: ^3.11.5 From 981cf100489e73e84e082fa34190524e2f910398 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 12:48:55 +0200 Subject: [PATCH 3/8] fix(social): cap relay connect with a timeout so one hung relay can't stall the pool --- .../lib/src/social/nostr/relay_pool.dart | 21 +++++++++--- .../test/social/relay_pool_test.dart | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/commons_core/lib/src/social/nostr/relay_pool.dart b/packages/commons_core/lib/src/social/nostr/relay_pool.dart index eac038d..aad754d 100644 --- a/packages/commons_core/lib/src/social/nostr/relay_pool.dart +++ b/packages/commons_core/lib/src/social/nostr/relay_pool.dart @@ -26,19 +26,32 @@ class RelayPool implements NostrChannel { /// Number of relays currently connected (for diagnostics/tests). int get relayCount => _connections.length; - /// Connects to each of [relayUrls], skipping any that fail. Throws - /// [StateError] only when NONE are reachable, so the caller can treat that as - /// "offline". + /// Connects to each of [relayUrls], skipping any that fail or take longer + /// than [connectTimeout] — a relay on a silently-filtering network would + /// otherwise hang the whole pool for minutes. Throws [StateError] only when + /// NONE are reachable, so the caller can treat that as "offline". static Future connect( List relayUrls, { required NostrIdentity identity, + Duration connectTimeout = const Duration(seconds: 10), }) async { final connections = []; await Future.wait( relayUrls.map((url) async { try { + final attempt = NostrConnection.connect(url, identity: identity); connections.add( - await NostrConnection.connect(url, identity: identity), + await attempt.timeout( + connectTimeout, + onTimeout: () { + // Abandon the hung attempt; if it ever completes, close it so + // the socket doesn't leak. + unawaited( + attempt.then((c) => c.close()).catchError((_) {}), + ); + throw TimeoutException('relay connect timed out', connectTimeout); + }, + ), ); } catch (_) { // Unreachable relay — skip it, keep the rest. diff --git a/packages/commons_core/test/social/relay_pool_test.dart b/packages/commons_core/test/social/relay_pool_test.dart index 52de12f..cdb41bc 100644 --- a/packages/commons_core/test/social/relay_pool_test.dart +++ b/packages/commons_core/test/social/relay_pool_test.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:typed_data'; import 'package:commons_core/commons_core.dart'; @@ -58,6 +59,38 @@ void main() { await r1.stop(); }); + test('a hung relay cannot stall the pool past the connect timeout', () async { + // A server that accepts TCP but never answers the WebSocket handshake — + // the shape of a silently-filtered network, where connect hangs for minutes. + final hang = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final held = []; + hang.listen(held.add); + final r1 = await MiniRelay.start(); + final alice = await idFor(5); + + final sw = Stopwatch()..start(); + final pool = await RelayPool.connect( + [r1.url, 'ws://127.0.0.1:${hang.port}'], + identity: alice, + connectTimeout: const Duration(milliseconds: 500), + ); + sw.stop(); + + expect(pool.relayCount, 1, reason: 'the live relay is kept'); + expect( + sw.elapsed, + lessThan(const Duration(seconds: 5)), + reason: 'the hung relay must not stall the whole pool', + ); + + await pool.close(); + for (final s in held) { + s.destroy(); + } + await hang.close(); + await r1.stop(); + }); + test('throws when no relay is reachable (caller treats as offline)', () async { final alice = await idFor(4); expect( From 8a7dc8d3dc066135641a7840fd3349c9a2ccd9e1 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 12:57:24 +0200 Subject: [PATCH 4/8] fix(market): recover by itself when the shared connection comes up 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) --- .../lib/services/social_connection.dart | 51 +++++++- apps/app_seeds/lib/state/offers_cubit.dart | 42 ++++++- apps/app_seeds/lib/ui/market_screen.dart | 6 +- .../test/services/social_connection_test.dart | 65 ++++++++++ .../test/state/offers_cubit_test.dart | 111 ++++++++++++++++++ .../social/nostr/nostr_offer_transport.dart | 7 +- 6 files changed, 273 insertions(+), 9 deletions(-) diff --git a/apps/app_seeds/lib/services/social_connection.dart b/apps/app_seeds/lib/services/social_connection.dart index 8e57949..332621c 100644 --- a/apps/app_seeds/lib/services/social_connection.dart +++ b/apps/app_seeds/lib/services/social_connection.dart @@ -25,19 +25,36 @@ class SocialConnection { required SocialSettings settings, SessionOpener? open, Stream? online, + List? retrySchedule, }) : _settings = settings, _open = open ?? social.openSession, - _online = online; + _online = online, + _retrySchedule = retrySchedule ?? _defaultRetrySchedule; + + /// Backoff for self-retries after a failed attempt while started and not + /// knowingly offline (the fresh-install case: online the whole time, first + /// connect fails, so no connectivity change ever retriggers a connect). + static const _defaultRetrySchedule = [ + Duration(seconds: 5), + Duration(seconds: 15), + Duration(seconds: 45), + Duration(seconds: 90), + ]; final SocialSettings _settings; final SessionOpener _open; final Stream? _online; + final List _retrySchedule; final _sessions = StreamController.broadcast(); SocialSession? _current; Future? _pending; StreamSubscription? _onlineSub; bool _disposed = false; + bool _started = false; + bool _knownOffline = false; + Timer? _retryTimer; + int _retryIndex = 0; /// Emits the live session on each (re)connect, and null when it drops. Stream get sessions => _sessions.stream; @@ -48,10 +65,14 @@ class SocialConnection { /// Begins watching connectivity (reconnect on regain, drop when offline) and /// attempts an initial connect. Idempotent-ish; call once at startup. void start() { + _started = true; _onlineSub = (_online ?? _connectivityOnline()).listen((isOnline) { + _knownOffline = !isOnline; if (!isOnline) { + _cancelRetry(); _drop(); } else if (_current == null) { + _retryIndex = 0; // fresh network — start the backoff over unawaited(session()); } }); @@ -75,15 +96,40 @@ class SocialConnection { return null; } _current = s; + _retryIndex = 0; + _cancelRetry(); _sessions.add(s); return s; } catch (_) { - return null; // unreachable — a later connectivity change retries + _scheduleRetry(); // unreachable — retry with backoff (see below) + return null; } finally { _pending = null; } } + /// After a failed attempt, retries by itself while started and not knowingly + /// offline — a connectivity change may never come if the device was online + /// all along. Un-started connections (one-shot [session] callers, tests) + /// never leave a timer behind. + void _scheduleRetry() { + if (!_started || _disposed || _knownOffline || _current != null) return; + _cancelRetry(); + final i = _retryIndex < _retrySchedule.length + ? _retryIndex + : _retrySchedule.length - 1; + _retryIndex++; + _retryTimer = Timer(_retrySchedule[i], () { + if (_disposed || _current != null) return; + unawaited(session()); + }); + } + + void _cancelRetry() { + _retryTimer?.cancel(); + _retryTimer = null; + } + void _drop() { final s = _current; _current = null; @@ -95,6 +141,7 @@ class SocialConnection { Future dispose() async { _disposed = true; + _cancelRetry(); await _onlineSub?.cancel(); _onlineSub = null; _drop(); diff --git a/apps/app_seeds/lib/state/offers_cubit.dart b/apps/app_seeds/lib/state/offers_cubit.dart index d6c462b..60e1c36 100644 --- a/apps/app_seeds/lib/state/offers_cubit.dart +++ b/apps/app_seeds/lib/state/offers_cubit.dart @@ -10,6 +10,7 @@ import '../services/offer_mapper.dart'; import '../services/offer_outbox.dart'; import '../services/offer_thumbnail.dart'; import '../services/social_connection.dart'; +import '../services/social_service.dart'; /// State of the offer discovery/publish screen. Transport-agnostic — it holds /// only what the UI shows, never a relay handle. @@ -28,6 +29,7 @@ class OffersState extends Equatable { this.nextPageCursor, this.publishing = false, this.hasSearched = false, + this.connectionEpoch = 0, this.error, }); @@ -80,6 +82,11 @@ class OffersState extends Equatable { /// from "searched, found nothing". final bool hasSearched; + /// Bumped whenever the underlying transport comes or goes, so the UI rebuilds + /// and re-reads [OffersCubit.isOnline] — states differing only here are + /// otherwise Equatable-equal and bloc would skip the emit. + final int connectionEpoch; + /// Last error, in human terms for the UI (null when fine). final String? error; @@ -136,6 +143,7 @@ class OffersState extends Equatable { int? Function()? nextPageCursor, bool? publishing, bool? hasSearched, + int? connectionEpoch, String? Function()? error, }) { return OffersState( @@ -153,6 +161,7 @@ class OffersState extends Equatable { nextPageCursor != null ? nextPageCursor() : this.nextPageCursor, publishing: publishing ?? this.publishing, hasSearched: hasSearched ?? this.hasSearched, + connectionEpoch: connectionEpoch ?? this.connectionEpoch, error: error != null ? error() : this.error, ); } @@ -172,6 +181,7 @@ class OffersState extends Equatable { nextPageCursor, publishing, hasSearched, + connectionEpoch, error, ]; } @@ -183,15 +193,40 @@ class OffersState extends Equatable { class OffersCubit extends Cubit { OffersCubit( this._transport, { + SocialConnection? connection, Future Function(String varietyId)? coverPhoto, String? Function(Uint8List bytes)? thumbnail, Future Function()? onDispose, }) : _coverPhoto = coverPhoto, _thumbnail = thumbnail, _onDispose = onDispose, - super(const OffersState()); + super(const OffersState()) { + // Fresh-install fix: the transport captured at build time may be null while + // the shared connection is still coming up (or dropped). Follow the + // connection so the market recovers by itself instead of staying offline + // until a manual retry. + _connSub = connection?.sessions.listen(_onSession); + } - final OfferTransport? _transport; + OfferTransport? _transport; + StreamSubscription? _connSub; + + /// The last area prefix asked of [discover]; re-run when the connection + /// (re)appears so results show without the user tapping anything. + String? _lastPrefix; + + void _onSession(SocialSession? session) { + if (isClosed) return; + final transport = session?.offers; + if (identical(transport, _transport)) return; + _transport = transport; + final prefix = _lastPrefix; + if (transport != null && prefix != null) { + unawaited(discover(prefix)); // emits fresh states as results arrive + } else { + emit(state.copyWith(connectionEpoch: state.connectionEpoch + 1)); + } + } /// Fetches a variety's cover photo bytes; null in tests or when no inventory /// repo is wired. @@ -225,6 +260,7 @@ class OffersCubit extends Cubit { /// from now on — older results arrive via [loadNextPage], not by draining the /// whole area into memory. Future discover(String geohashPrefix) async { + _lastPrefix = geohashPrefix; final transport = _transport; if (transport == null) { emit(state.copyWith(error: () => 'offline', hasSearched: true)); @@ -467,6 +503,7 @@ class OffersCubit extends Cubit { @override Future close() async { _searchTimeout?.cancel(); + await _connSub?.cancel(); await _sub?.cancel(); await _onDispose?.call(); return super.close(); @@ -484,6 +521,7 @@ Future createOffersCubit( final session = await connection.session(); return OffersCubit( session?.offers, + connection: connection, // keeps following (re)connects — see _onSession coverPhoto: repository?.coverPhotoFor, thumbnail: offerThumbnailDataUri, ); diff --git a/apps/app_seeds/lib/ui/market_screen.dart b/apps/app_seeds/lib/ui/market_screen.dart index 23941a5..5b6e0ed 100644 --- a/apps/app_seeds/lib/ui/market_screen.dart +++ b/apps/app_seeds/lib/ui/market_screen.dart @@ -122,10 +122,10 @@ class _MarketScreenState extends State { _loading = false; }); await previous?.close(); - if (area.isNotEmpty && cubit.isOnline) { + if (area.isNotEmpty) { // Flush anything parked while offline, then show what's out there. final outbox = widget.outbox; - if (outbox != null && repo != null) { + if (outbox != null && repo != null && cubit.isOnline) { await flushOutbox( outbox: outbox, cubit: cubit, @@ -136,6 +136,8 @@ class _MarketScreenState extends State { } if (mounted) { final precision = await widget.settings.searchPrecision(); + // Even when offline: this records the wanted area on the cubit, which + // re-runs the discovery by itself once the connection comes up. if (mounted) await cubit.discover(searchPrefix(area, precision)); } } diff --git a/apps/app_seeds/test/services/social_connection_test.dart b/apps/app_seeds/test/services/social_connection_test.dart index e4c7151..7b856de 100644 --- a/apps/app_seeds/test/services/social_connection_test.dart +++ b/apps/app_seeds/test/services/social_connection_test.dart @@ -45,11 +45,13 @@ void main() { required List opened, Stream? online, bool Function()? fail, + List? retrySchedule, }) => SocialConnection( social: social, settings: settings, online: online, + retrySchedule: retrySchedule, open: (_) async { if (fail?.call() ?? false) throw StateError('unreachable'); final ch = FakeChannel(); @@ -106,6 +108,69 @@ void main() { await online.close(); }); + test( + 'started connection retries by itself after a failed first attempt, ' + 'without any connectivity event', () async { + // The fresh-install case: device online the whole time, but the very first + // connect fails (cold DNS/TLS). The connectivity stream never fires, so + // only the backoff retry can bring the market up without a manual Retry. + final opened = []; + var down = true; + final conn = make( + opened: opened, + online: const Stream.empty(), // connectivity never speaks + fail: () => down, + retrySchedule: const [Duration(milliseconds: 5)], + ); + final emitted = []; + conn.sessions.listen(emitted.add); + + conn.start(); + await Future.delayed(Duration.zero); + expect(conn.current, isNull); // first attempt failed + + down = false; // network path recovers + await Future.delayed(const Duration(milliseconds: 100)); + expect(conn.current, isNotNull, reason: 'backoff retry reconnected'); + expect(opened, hasLength(1)); + expect(emitted.last, isNotNull, reason: 'recovery announced on sessions'); + + await conn.dispose(); + }); + + test('a plain session() failure schedules no retry when never started', + () async { + // Widget tests build cubits against an un-started connection; a failed + // one-shot session() must not leave a pending retry timer behind. + final opened = []; + final conn = make( + opened: opened, + fail: () => true, + retrySchedule: const [Duration(milliseconds: 5)], + ); + expect(await conn.session(), isNull); + await Future.delayed(const Duration(milliseconds: 50)); + expect(opened, isEmpty); // no background retry fired + await conn.dispose(); + }); + + test('dispose cancels a pending backoff retry', () async { + final opened = []; + var down = true; + final conn = make( + opened: opened, + online: const Stream.empty(), + fail: () => down, + retrySchedule: const [Duration(milliseconds: 20)], + ); + conn.start(); + await Future.delayed(Duration.zero); // first attempt fails + await conn.dispose(); // cancels the scheduled retry + down = false; + await Future.delayed(const Duration(milliseconds: 100)); + expect(opened, isEmpty, reason: 'no reconnect after dispose'); + }); + test('returns null when the relay is unreachable, and retries later', () async { final opened = []; diff --git a/apps/app_seeds/test/state/offers_cubit_test.dart b/apps/app_seeds/test/state/offers_cubit_test.dart index 650088a..4c84dc6 100644 --- a/apps/app_seeds/test/state/offers_cubit_test.dart +++ b/apps/app_seeds/test/state/offers_cubit_test.dart @@ -7,7 +7,11 @@ import 'package:tane/data/variety_repository.dart'; import 'package:tane/db/enums.dart'; import 'package:tane/services/discovery_area.dart'; import 'package:tane/services/offer_mapper.dart'; +import 'package:nostr/nostr.dart'; import 'package:tane/services/offer_outbox.dart'; +import 'package:tane/services/social_connection.dart'; +import 'package:tane/services/social_service.dart'; +import 'package:tane/services/social_settings.dart'; import 'package:tane/state/offers_cubit.dart'; import '../support/test_support.dart'; @@ -835,4 +839,111 @@ void main() { await cubit.close(); }); }); + + group('connection recovery (fresh-install fix)', () { + const seedHex = + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'; + + late SocialService social; + late SocialSettings settings; + + setUp(() async { + social = await SocialService.fromRootSeedHex(seedHex); + settings = SocialSettings(InMemorySecretStore()); + }); + + SocialConnection makeConnection({ + required bool Function() down, + Stream? online, + }) => + SocialConnection( + social: social, + settings: settings, + online: online ?? const Stream.empty(), + retrySchedule: const [Duration(milliseconds: 5)], + open: (_) async { + if (down()) throw StateError('unreachable'); + return SocialSession(_NoopChannel()); + }, + ); + + test('comes online by itself when the shared connection connects later', + () async { + var down = true; + final conn = makeConnection(down: () => down); + final cubit = await createOffersCubit(conn); + expect(cubit.isOnline, isFalse); // built while unreachable + + final epochBefore = cubit.state.connectionEpoch; + conn.start(); + down = false; // network path recovers; backoff retry reconnects + await Future.delayed(const Duration(milliseconds: 100)); + + expect(cubit.isOnline, isTrue, reason: 'no manual Retry needed'); + expect(cubit.state.connectionEpoch, greaterThan(epochBefore), + reason: 'a state was emitted so the UI re-reads isOnline'); + + await cubit.close(); + await conn.dispose(); + }); + + test('re-runs the last discovery when the connection recovers', () async { + var down = true; + final conn = makeConnection(down: () => down); + final cubit = await createOffersCubit(conn); + + await cubit.discover('sp3e9'); // offline → error, but the wish is kept + expect(cubit.state.error, 'offline'); + + conn.start(); + down = false; + await Future.delayed(const Duration(milliseconds: 100)); + + expect(cubit.isOnline, isTrue); + expect(cubit.state.areaGeohash, 'sp3e9', reason: 'discovery re-ran'); + expect(cubit.state.hasSearched, isTrue); + expect(cubit.state.error, isNull, reason: 'offline error cleared'); + + await cubit.close(); + await conn.dispose(); + }); + + test('goes offline (and tells the UI) when the session drops', () async { + final online = StreamController.broadcast(); + final conn = makeConnection(down: () => false, online: online.stream); + conn.start(); + await pumpEventQueue(); + final cubit = await createOffersCubit(conn); + expect(cubit.isOnline, isTrue); + + final epochBefore = cubit.state.connectionEpoch; + online.add(false); // network lost + await pumpEventQueue(); + + expect(cubit.isOnline, isFalse); + expect(cubit.state.connectionEpoch, greaterThan(epochBefore)); + + await cubit.close(); + await conn.dispose(); + await online.close(); + }); + }); +} + +/// A no-op [NostrChannel]: just enough to build a [SocialSession] whose offer +/// transport answers with empty results. +class _NoopChannel implements NostrChannel { + @override + String get privateKeyHex => '00' * 32; + @override + String get publicKeyHex => 'ab' * 32; + @override + Future<({bool accepted, String message})> publish(Event event) async => + (accepted: true, message: ''); + @override + Stream subscribe(Filter filter) => const Stream.empty(); + @override + Future> reqOnce(Filter filter) async => const []; + @override + Future close() async {} } diff --git a/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart b/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart index 305f842..bd4c9d8 100644 --- a/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart +++ b/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart @@ -46,9 +46,10 @@ class NostrOfferTransport implements OfferTransport { @override Future discoverPage(DiscoveryQuery query) async { - final events = await _conn.reqOnce(_filter(query)) - // Newest first; the relays dedup within themselves and reqOnce dedups - // across them, but order is not guaranteed, so sort here. + // Newest first; the relays dedup within themselves and reqOnce dedups + // across them, but order is not guaranteed, so sort here — on a copy, as + // the channel may hand out an unmodifiable list. + final events = [...await _conn.reqOnce(_filter(query))] ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); final offers = []; for (final e in events) { From 13b6b4bfce471f133e3e6360ffab5b8154cbd021 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 13:03:25 +0200 Subject: [PATCH 5/8] fix(market): zone prompt before connection error; keep Save above the nav bar - 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 --- apps/app_seeds/lib/ui/market_screen.dart | 479 ++++++++++-------- .../app_seeds/test/ui/market_screen_test.dart | 53 +- 2 files changed, 318 insertions(+), 214 deletions(-) diff --git a/apps/app_seeds/lib/ui/market_screen.dart b/apps/app_seeds/lib/ui/market_screen.dart index 5b6e0ed..b172ca2 100644 --- a/apps/app_seeds/lib/ui/market_screen.dart +++ b/apps/app_seeds/lib/ui/market_screen.dart @@ -100,11 +100,11 @@ class _MarketScreenState extends State { Future _init() async { // Only needed to flush the outbox; read here (while mounted) to avoid using // context across the awaits below. Null when there's no outbox (e.g. tests). - final repo = - widget.outbox != null ? context.read() : null; + final repo = widget.outbox != null + ? context.read() + : null; setState(() => _loading = true); - final cubit = - await createOffersCubit(widget.connection, repository: repo); + final cubit = await createOffersCubit(widget.connection, repository: repo); cubit.setBlockedAuthors(await widget.settings.blockedPubkeys()); cubit.setHiddenOffers(await widget.settings.hiddenOfferKeys()); final area = await widget.settings.areaGeohash(); @@ -208,12 +208,18 @@ class _MarketScreenState extends State { await widget.outbox?.remove(ids); // published now, so unpark any duplicates // count == 0 with lots to share means every relay refused — say so plainly // rather than "shared 0", which reads like success. - messenger.showSnackBar(SnackBar( - content: Text(count == 0 ? t.market.shareFailed : t.market.sharedCount(n: count)), - )); + messenger.showSnackBar( + SnackBar( + content: Text( + count == 0 ? t.market.shareFailed : t.market.sharedCount(n: count), + ), + ), + ); final precision = await widget.settings.searchPrecision(); if (!mounted) return; - await cubit.discover(searchPrefix(area, precision)); // refresh incl. just-shared + await cubit.discover( + searchPrefix(area, precision), + ); // refresh incl. just-shared } @override @@ -242,13 +248,13 @@ class _MarketScreenState extends State { ), floatingActionButton: cubit != null && (cubit.isOnline || widget.outbox != null) - ? FloatingActionButton.extended( - key: const Key('market.shareMine'), - onPressed: _shareMine, - icon: const Icon(Icons.campaign_outlined), - label: Text(t.market.shareMine), - ) - : null, + ? FloatingActionButton.extended( + key: const Key('market.shareMine'), + onPressed: _shareMine, + icon: const Icon(Icons.campaign_outlined), + label: Text(t.market.shareMine), + ) + : null, body: _loading || cubit == null ? const Center(child: CircularProgressIndicator()) : BlocProvider.value( @@ -300,6 +306,17 @@ class MarketBody extends StatelessWidget { final t = context.t; return BlocBuilder( builder: (context, state) { + // A new user's first step is setting a zone — which works offline — so + // that prompt wins over the connection error. + if (!hasArea) { + return _EmptyState( + icon: Icons.place_outlined, + title: t.market.setArea, + body: t.market.setAreaBody, + actionLabel: t.market.setUp, + onAction: onConfigure, + ); + } if (!context.read().isOnline) { // Default community servers exist, so "offline" means unreachable — // offer a retry rather than "set up sharing". @@ -311,15 +328,6 @@ class MarketBody extends StatelessWidget { onAction: onRetry, ); } - if (!hasArea) { - return _EmptyState( - icon: Icons.place_outlined, - title: t.market.setArea, - body: t.market.setAreaBody, - actionLabel: t.market.setUp, - onAction: onConfigure, - ); - } if (state.searching && state.offers.isEmpty) { return Center( child: Column( @@ -327,8 +335,10 @@ class MarketBody extends StatelessWidget { children: [ const CircularProgressIndicator(), const SizedBox(height: 16), - Text(t.market.searching, - style: const TextStyle(color: seedMuted)), + Text( + t.market.searching, + style: const TextStyle(color: seedMuted), + ), ], ), ); @@ -372,16 +382,22 @@ class MarketBody extends StatelessWidget { prefixIcon: const Icon(Icons.search, color: seedMuted), // Offer to save the current search once it's worth alerting on // (some text typed or a chip active), never for the bare list. - suffixIcon: savedSearches != null && + suffixIcon: + savedSearches != null && (state.query.trim().isNotEmpty || state.hasActiveFilter) ? IconButton( key: const Key('market.saveSearch'), - icon: const Icon(Icons.bookmark_add_outlined, - color: seedGreen), + icon: const Icon( + Icons.bookmark_add_outlined, + color: seedGreen, + ), tooltip: t.savedSearches.save, - onPressed: () => - _saveCurrentSearch(context, savedSearches!, state), + onPressed: () => _saveCurrentSearch( + context, + savedSearches!, + state, + ), ) : null, hintStyle: const TextStyle(color: seedMuted), @@ -433,13 +449,17 @@ class MarketBody extends StatelessWidget { physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), // A trailing spinner row while the next page loads. - itemCount: visible.length + (state.loadingMore ? 1 : 0), - separatorBuilder: (_, _) => const SizedBox(height: 12), + itemCount: + visible.length + (state.loadingMore ? 1 : 0), + separatorBuilder: (_, _) => + const SizedBox(height: 12), itemBuilder: (context, i) { if (i >= visible.length) { return const Padding( padding: EdgeInsets.symmetric(vertical: 16), - child: Center(child: CircularProgressIndicator()), + child: Center( + child: CircularProgressIndicator(), + ), ); } final o = visible[i]; @@ -576,7 +596,9 @@ class _OfferCard extends StatelessWidget { if (mine) ...[ Container( padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 3), + horizontal: 8, + vertical: 3, + ), decoration: BoxDecoration( color: seedGreen, borderRadius: BorderRadius.circular(20), @@ -600,8 +622,10 @@ class _OfferCard extends StatelessWidget { children: [ const Icon(Icons.place_outlined, size: 15, color: seedMuted), const SizedBox(width: 4), - Text(t.market.near, - style: const TextStyle(color: seedMuted, fontSize: 13)), + Text( + t.market.near, + style: const TextStyle(color: seedMuted, fontSize: 13), + ), if (offer.isOrganic) ...[ const SizedBox(width: 10), const Icon(Icons.eco, size: 15, color: seedGreen), @@ -609,7 +633,8 @@ class _OfferCard extends StatelessWidget { const Spacer(), if (offer.type == OfferType.sale && offer.priceAmount != null) Text( - '${offer.priceAmount} ${offer.priceCurrency ?? ''}'.trim(), + '${offer.priceAmount} ${offer.priceCurrency ?? ''}' + .trim(), style: const TextStyle( color: seedOnSurface, fontWeight: FontWeight.w600, @@ -908,194 +933,226 @@ class _ConfigSheetState extends State<_ConfigSheet> { Widget build(BuildContext context) { final t = context.t; final hasArea = _area.text.trim().isNotEmpty; - return Padding( - padding: EdgeInsets.only( - left: 20, - right: 20, - top: 20, - bottom: MediaQuery.of(context).viewInsets.bottom + 20, - ), - child: _loading - ? const SizedBox( - height: 120, child: Center(child: CircularProgressIndicator())) - : SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - t.market.configTitle, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: seedTitle, - ), - ), - const SizedBox(height: 8), - Text( - t.market.setupIntro, - style: const TextStyle( - color: seedMuted, fontSize: 13, height: 1.4), - ), - const SizedBox(height: 18), - // Setting your area from device location is the human path; - // typing a code is the advanced fallback. - if (widget.location != null) - FilledButton.tonalIcon( - key: const Key('market.useLocation'), - onPressed: _locationBusy ? null : _useLocation, - icon: _locationBusy - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.my_location, size: 18), - label: Text(t.market.useLocation), - ), - if (_locationError != null) - Padding( - padding: const EdgeInsets.only(top: 6), - child: Text( - _locationError!, - style: const TextStyle( - color: Color(0xFFB3261E), fontSize: 12), + // SafeArea keeps the Save button above the system nav bar on edge-to-edge + // devices; the viewInsets padding still lifts it above the keyboard. + return SafeArea( + top: false, + child: Padding( + padding: EdgeInsets.only( + left: 20, + right: 20, + top: 20, + bottom: MediaQuery.of(context).viewInsets.bottom + 20, + ), + child: _loading + ? const SizedBox( + height: 120, + child: Center(child: CircularProgressIndicator()), + ) + : SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + t.market.configTitle, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: seedTitle, ), ), - const SizedBox(height: 12), - Row( - children: [ - Icon( - hasArea - ? Icons.check_circle - : Icons.pending_outlined, - size: 18, - color: hasArea ? seedGreen : seedMuted, + const SizedBox(height: 8), + Text( + t.market.setupIntro, + style: const TextStyle( + color: seedMuted, + fontSize: 13, + height: 1.4, ), - const SizedBox(width: 8), - Expanded( + ), + const SizedBox(height: 18), + // Setting your area from device location is the human path; + // typing a code is the advanced fallback. + if (widget.location != null) + FilledButton.tonalIcon( + key: const Key('market.useLocation'), + onPressed: _locationBusy ? null : _useLocation, + icon: _locationBusy + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.my_location, size: 18), + label: Text(t.market.useLocation), + ), + if (_locationError != null) + Padding( + padding: const EdgeInsets.only(top: 6), child: Text( - hasArea ? t.market.areaSet : t.market.areaNotSet, - style: TextStyle( - color: hasArea ? seedOnSurface : seedMuted, - fontSize: 13, + _locationError!, + style: const TextStyle( + color: Color(0xFFB3261E), + fontSize: 12, ), ), ), - ], - ), - const SizedBox(height: 16), - Align( - alignment: AlignmentDirectional.centerStart, - child: Text( - t.market.rangeLabel, - style: const TextStyle(fontSize: 13, color: seedMuted), - ), - ), - const SizedBox(height: 8), - SegmentedButton( - key: const Key('market.range'), - segments: [ - ButtonSegment(value: 5, label: Text(t.market.rangeNear)), - ButtonSegment(value: 4, label: Text(t.market.rangeArea)), - ButtonSegment(value: 3, label: Text(t.market.rangeRegion)), - ], - selected: {_precision}, - showSelectedIcon: false, - onSelectionChanged: (s) => - setState(() => _precision = s.first), - ), - const SizedBox(height: 8), - Theme( - data: Theme.of(context) - .copyWith(dividerColor: Colors.transparent), - child: ExpansionTile( - key: const Key('market.advanced'), - tilePadding: EdgeInsets.zero, - childrenPadding: const EdgeInsets.only(bottom: 8), - title: Text( - t.market.advanced, - style: const TextStyle(fontSize: 13, color: seedMuted), - ), + const SizedBox(height: 12), + Row( children: [ - TextField( - key: const Key('market.area'), - controller: _area, - onChanged: (_) => setState(() {}), - decoration: InputDecoration( - labelText: t.market.areaCodeLabel, - helperText: t.market.areaCodeHint, - helperMaxLines: 2, - ), + Icon( + hasArea ? Icons.check_circle : Icons.pending_outlined, + size: 18, + color: hasArea ? seedGreen : seedMuted, ), - const SizedBox(height: 14), - Align( - alignment: AlignmentDirectional.centerStart, + const SizedBox(width: 8), + Expanded( child: Text( - t.market.serversLabel, - style: const TextStyle( - fontSize: 13, color: seedMuted), - ), - ), - Padding( - padding: const EdgeInsetsDirectional.only( - top: 2, bottom: 4), - child: Text( - t.market.serversHelp, - style: const TextStyle( - fontSize: 12, color: seedMuted, height: 1.3), - ), - ), - for (final url in _knownRelays) - CheckboxListTile( - key: Key('market.server.$url'), - contentPadding: EdgeInsets.zero, - dense: true, - controlAffinity: ListTileControlAffinity.leading, - value: _selectedRelays.contains(url), - title: Text(_relayLabel(url)), - onChanged: (on) => setState(() { - if (on ?? false) { - _selectedRelays.add(url); - } else { - _selectedRelays.remove(url); - } - }), - ), - Align( - alignment: AlignmentDirectional.centerStart, - child: TextButton.icon( - key: const Key('market.addServer'), - onPressed: _addServer, - icon: const Icon(Icons.add, size: 18), - label: Text(t.market.serversAdvanced), - ), - ), - ListTile( - key: const Key('market.blockedPeople'), - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.block, color: seedMuted), - title: Text(t.block.manageTitle), - trailing: const Icon(Icons.chevron_right), - onTap: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (_) => - BlockedPeopleSheet(settings: widget.settings), + hasArea ? t.market.areaSet : t.market.areaNotSet, + style: TextStyle( + color: hasArea ? seedOnSurface : seedMuted, + fontSize: 13, + ), ), ), ], ), - ), - const SizedBox(height: 20), - FilledButton( - key: const Key('market.save'), - onPressed: _save, - child: Text(t.market.save), - ), - ], + const SizedBox(height: 16), + Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + t.market.rangeLabel, + style: const TextStyle(fontSize: 13, color: seedMuted), + ), + ), + const SizedBox(height: 8), + SegmentedButton( + key: const Key('market.range'), + segments: [ + ButtonSegment( + value: 5, + label: Text(t.market.rangeNear), + ), + ButtonSegment( + value: 4, + label: Text(t.market.rangeArea), + ), + ButtonSegment( + value: 3, + label: Text(t.market.rangeRegion), + ), + ], + selected: {_precision}, + showSelectedIcon: false, + onSelectionChanged: (s) => + setState(() => _precision = s.first), + ), + const SizedBox(height: 8), + Theme( + data: Theme.of( + context, + ).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + key: const Key('market.advanced'), + tilePadding: EdgeInsets.zero, + childrenPadding: const EdgeInsets.only(bottom: 8), + title: Text( + t.market.advanced, + style: const TextStyle( + fontSize: 13, + color: seedMuted, + ), + ), + children: [ + TextField( + key: const Key('market.area'), + controller: _area, + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + labelText: t.market.areaCodeLabel, + helperText: t.market.areaCodeHint, + helperMaxLines: 2, + ), + ), + const SizedBox(height: 14), + Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + t.market.serversLabel, + style: const TextStyle( + fontSize: 13, + color: seedMuted, + ), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only( + top: 2, + bottom: 4, + ), + child: Text( + t.market.serversHelp, + style: const TextStyle( + fontSize: 12, + color: seedMuted, + height: 1.3, + ), + ), + ), + for (final url in _knownRelays) + CheckboxListTile( + key: Key('market.server.$url'), + contentPadding: EdgeInsets.zero, + dense: true, + controlAffinity: ListTileControlAffinity.leading, + value: _selectedRelays.contains(url), + title: Text(_relayLabel(url)), + onChanged: (on) => setState(() { + if (on ?? false) { + _selectedRelays.add(url); + } else { + _selectedRelays.remove(url); + } + }), + ), + Align( + alignment: AlignmentDirectional.centerStart, + child: TextButton.icon( + key: const Key('market.addServer'), + onPressed: _addServer, + icon: const Icon(Icons.add, size: 18), + label: Text(t.market.serversAdvanced), + ), + ), + ListTile( + key: const Key('market.blockedPeople'), + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.block, color: seedMuted), + title: Text(t.block.manageTitle), + trailing: const Icon(Icons.chevron_right), + onTap: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => + BlockedPeopleSheet(settings: widget.settings), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + FilledButton( + key: const Key('market.save'), + onPressed: _save, + child: Text(t.market.save), + ), + ], + ), ), - ), + ), ); } } diff --git a/apps/app_seeds/test/ui/market_screen_test.dart b/apps/app_seeds/test/ui/market_screen_test.dart index b063201..5f99e18 100644 --- a/apps/app_seeds/test/ui/market_screen_test.dart +++ b/apps/app_seeds/test/ui/market_screen_test.dart @@ -84,8 +84,9 @@ void main() { findsOneWidget); }); - testWidgets('MarketScreen with no relays configured degrades to offline', - (tester) async { + testWidgets( + 'a fresh install (no area, unreachable) asks for the area first, ' + 'not the connection', (tester) async { final social = await SocialService.fromRootSeedHex('00' * 32); final settings = SocialSettings(InMemorySecretStore()); await settings.setRelayUrls(const []); // offline: don't hit the network @@ -94,7 +95,23 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Seeds near you'), findsOneWidget); // app bar title - expect(find.text('Retry'), findsOneWidget); // can't-reach state offers retry + // Setting a zone works offline and is the first step — the connection + // error must not bury it. + expect(find.text('Set your area'), findsOneWidget); + expect(find.text('Retry'), findsNothing); + }); + + testWidgets('with an area set, unreachable servers degrade to retry', + (tester) async { + final social = await SocialService.fromRootSeedHex('00' * 32); + final settings = SocialSettings(InMemorySecretStore()); + await settings.setRelayUrls(const []); // offline: don't hit the network + await settings.setAreaGeohash('ezsn9'); + + await tester.pumpWidget(_wrapMarket(social, settings)); + await tester.pumpAndSettle(); + + expect(find.text('Retry'), findsOneWidget); }); testWidgets('the range selector persists the chosen search precision', @@ -148,6 +165,36 @@ void main() { expect(tester.widget(find.byKey(firstKey)).value, isTrue); }); + testWidgets('the config sheet Save button stays above the system nav bar', + (tester) async { + // Edge-to-edge Android: a 50-logical-px gesture/nav bar at the bottom. + tester.view.padding = const FakeViewPadding(bottom: 150); // physical px + tester.view.viewPadding = const FakeViewPadding(bottom: 150); + addTearDown(tester.view.reset); + + final social = await SocialService.fromRootSeedHex('00' * 32); + final settings = SocialSettings(InMemorySecretStore()); + await settings.setRelayUrls(const []); // offline: don't hit the network + + await tester.pumpWidget(_wrapMarket(social, settings)); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('market.config'))); + await tester.pumpAndSettle(); + + final save = find.byKey(const Key('market.save')); + await tester.ensureVisible(save); + await tester.pumpAndSettle(); + + final screenHeight = tester.view.physicalSize.height / + tester.view.devicePixelRatio; // 600 on the default test surface + const navBarLogical = 150 / 3.0; // FakeViewPadding is physical px + expect( + tester.getRect(save).bottom, + lessThanOrEqualTo(screenHeight - navBarLogical + 0.1), + reason: 'Save must not sit under the system navigation bar', + ); + }); + testWidgets('"use my location" fills the area with a coarse geohash', (tester) async { final social = await SocialService.fromRootSeedHex('00' * 32); From 1a946d5c26dd9bb56c8ddf98b8d0b6c4681ee96a Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 13:03:25 +0200 Subject: [PATCH 6/8] =?UTF-8?q?fix(i18n):=20honest=20sharing=20intro=20?= =?UTF-8?q?=E2=80=94=20community=20servers=20exist,=20no=20company=20runs?= =?UTF-8?q?=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/app_seeds/lib/i18n/ast.i18n.json | 2 +- apps/app_seeds/lib/i18n/de.i18n.json | 2 +- apps/app_seeds/lib/i18n/en.i18n.json | 2 +- apps/app_seeds/lib/i18n/es.i18n.json | 2 +- apps/app_seeds/lib/i18n/fr.i18n.json | 2 +- apps/app_seeds/lib/i18n/pt.i18n.json | 2 +- apps/app_seeds/lib/i18n/pt_BR.i18n.json | 2 +- apps/app_seeds/lib/i18n/strings.g.dart | 2 +- apps/app_seeds/lib/i18n/strings_ast.g.dart | 4 ++-- apps/app_seeds/lib/i18n/strings_de.g.dart | 4 ++-- apps/app_seeds/lib/i18n/strings_en.g.dart | 6 +++--- apps/app_seeds/lib/i18n/strings_es.g.dart | 4 ++-- apps/app_seeds/lib/i18n/strings_fr.g.dart | 4 ++-- apps/app_seeds/lib/i18n/strings_pt.g.dart | 4 ++-- apps/app_seeds/lib/i18n/strings_pt_BR.g.dart | 4 ++-- 15 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/app_seeds/lib/i18n/ast.i18n.json b/apps/app_seeds/lib/i18n/ast.i18n.json index f27fa57..7b1a054 100644 --- a/apps/app_seeds/lib/i18n/ast.i18n.json +++ b/apps/app_seeds/lib/i18n/ast.i18n.json @@ -516,7 +516,7 @@ "contact": "Mensaxe", "mine": "Tu", "configTitle": "Configuración de compartir", - "setupIntro": "Compartir con xente cercano ye opcional. Namás indica la to zona averada — yá tas coneutáu a servidores comunitarios compartíos pa qu'otres persones atopen lo qu'ufiertes, ensin nenguna empresa en mediu.", + "setupIntro": "Compartir con xente cercano ye opcional. Namás indica la to zona averada — lo qu'ufiertes viaxa per servidores comunitarios, calteníos por persones y coleutivos, non por una empresa, pa qu'otres persones cercanes puedan atopalo.", "areaLabel": "La to zona", "areaHelp": "Caltiénse averao a costafecha — la to zona, enxamás un puntu esactu.", "areaSet": "La to zona ta puesta — averada, enxamás el to puntu esactu", diff --git a/apps/app_seeds/lib/i18n/de.i18n.json b/apps/app_seeds/lib/i18n/de.i18n.json index 6d63d63..d6230c7 100644 --- a/apps/app_seeds/lib/i18n/de.i18n.json +++ b/apps/app_seeds/lib/i18n/de.i18n.json @@ -519,7 +519,7 @@ "contact": "Nachricht", "mine": "Du", "configTitle": "Teilen-Einrichtung", - "setupIntro": "Teilen mit Menschen in der Nähe ist optional. Gib einfach dein ungefähres Gebiet an - du bist bereits mit freigegebenen Gemeinschaftsservern verbunden, damit Leute das finden können, das du anbietest, ohne ein Unternehmen dazwischen.", + "setupIntro": "Teilen mit Menschen in der Nähe ist optional. Gib einfach dein ungefähres Gebiet an - was du anbietest, läuft über Gemeinschaftsserver, die von Menschen und Kollektiven betrieben werden, nicht von einem Unternehmen, damit andere in deiner Nähe es finden können.", "areaLabel": "Dein Gebiet", "areaHelp": "Absichtlich grob gehalten - deine Zone, nie ein genauer Punkt.", "areaSet": "Dein Gebiet ist gesetzt - grob, nie dein genauer Punkt", diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 791f12d..e1caf31 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -541,7 +541,7 @@ "contact": "Message", "mine": "You", "configTitle": "Sharing setup", - "setupIntro": "Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle.", + "setupIntro": "Sharing with people nearby is optional. Just set your rough area — what you offer travels through community servers, run by people and collectives rather than a company, so others nearby can find it.", "areaLabel": "Your area", "areaHelp": "Kept rough on purpose — your zone, never an exact spot.", "areaSet": "Your area is set — kept coarse, never your exact spot", diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index d1faa15..5b1a570 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -540,7 +540,7 @@ "contact": "Mensaje", "mine": "Tú", "configTitle": "Configuración de compartir", - "setupIntro": "Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.", + "setupIntro": "Compartir con gente cercana es opcional. Solo indica tu zona aproximada — lo que ofreces viaja por servidores comunitarios, mantenidos por personas y colectivos, no por una empresa, para que otras personas cercanas puedan encontrarlo.", "areaLabel": "Tu zona", "areaHelp": "Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.", "areaSet": "Tu zona está puesta — aproximada, nunca tu punto exacto", diff --git a/apps/app_seeds/lib/i18n/fr.i18n.json b/apps/app_seeds/lib/i18n/fr.i18n.json index b56e42c..8b5b4b8 100644 --- a/apps/app_seeds/lib/i18n/fr.i18n.json +++ b/apps/app_seeds/lib/i18n/fr.i18n.json @@ -519,7 +519,7 @@ "contact": "Message", "mine": "Vous", "configTitle": "Configuration du partage", - "setupIntro": "Le partage avec les gens à proximité est optionnel. Définissez simplement votre zone approximative — vous êtes déjà connectés aux serveurs communautaires partagés pour que les gens trouvent ce que vous offrez, sans aucune entreprise au milieu.", + "setupIntro": "Le partage avec les gens à proximité est optionnel. Définissez simplement votre zone approximative — ce que vous offrez passe par des serveurs communautaires, tenus par des personnes et des collectifs et non par une entreprise, pour que d'autres près de chez vous puissent le trouver.", "areaLabel": "Votre zone", "areaHelp": "Gardée approximative volontairement — votre zone, jamais un point exact.", "areaSet": "Votre zone est définie — approximative, jamais votre point exact", diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index 2e78828..810b0d4 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -541,7 +541,7 @@ "contact": "Mensagem", "mine": "Tu", "configTitle": "Configuração de partilha", - "setupIntro": "Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.", + "setupIntro": "Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — o que ofereces viaja por servidores comunitários, mantidos por pessoas e coletivos, não por uma empresa, para que outras pessoas perto de ti o possam encontrar.", "areaLabel": "A tua zona", "areaHelp": "Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.", "areaSet": "A tua zona está definida — aproximada, nunca o teu ponto exato", diff --git a/apps/app_seeds/lib/i18n/pt_BR.i18n.json b/apps/app_seeds/lib/i18n/pt_BR.i18n.json index 4e9c920..bc4edba 100644 --- a/apps/app_seeds/lib/i18n/pt_BR.i18n.json +++ b/apps/app_seeds/lib/i18n/pt_BR.i18n.json @@ -541,7 +541,7 @@ "contact": "Mensagem", "mine": "Você", "configTitle": "Configuração de compartilha", - "setupIntro": "Compartilhar com pessoas por perto é opcional. Basta indicar a sua zona aproximada — já está conectado a servidores comunitários compartilhados para que outras pessoas encontrem o que você oferece, sem nenhuma empresa no meio.", + "setupIntro": "Compartilhar com pessoas por perto é opcional. Basta indicar a sua zona aproximada — o que você oferece viaja por servidores comunitários, mantidos por pessoas e coletivos, não por uma empresa, para que outras pessoas perto de você possam encontrar.", "areaLabel": "A sua zona", "areaHelp": "Mantém-se aproximado de propósito — a sua zona, nunca um ponto exato.", "areaSet": "A sua zona está definida — aproximada, nunca o seu ponto exato", diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 6acb839..0132a97 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 8 /// Strings: 4287 (535 per locale) /// -/// Built on 2026-07-20 at 21:06 UTC +/// Built on 2026-07-22 at 11:02 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_ast.g.dart b/apps/app_seeds/lib/i18n/strings_ast.g.dart index b2a316f..c3ee9bd 100644 --- a/apps/app_seeds/lib/i18n/strings_ast.g.dart +++ b/apps/app_seeds/lib/i18n/strings_ast.g.dart @@ -766,7 +766,7 @@ class _Translations$market$ast extends Translations$market$en { @override String get contact => 'Mensaxe'; @override String get mine => 'Tu'; @override String get configTitle => 'Configuración de compartir'; - @override String get setupIntro => 'Compartir con xente cercano ye opcional. Namás indica la to zona averada — yá tas coneutáu a servidores comunitarios compartíos pa qu\'otres persones atopen lo qu\'ufiertes, ensin nenguna empresa en mediu.'; + @override String get setupIntro => 'Compartir con xente cercano ye opcional. Namás indica la to zona averada — lo qu\'ufiertes viaxa per servidores comunitarios, calteníos por persones y coleutivos, non por una empresa, pa qu\'otres persones cercanes puedan atopalo.'; @override String get areaLabel => 'La to zona'; @override String get areaHelp => 'Caltiénse averao a costafecha — la to zona, enxamás un puntu esactu.'; @override String get areaSet => 'La to zona ta puesta — averada, enxamás el to puntu esactu'; @@ -1802,7 +1802,7 @@ extension on TranslationsAst { 'market.contact' => 'Mensaxe', 'market.mine' => 'Tu', 'market.configTitle' => 'Configuración de compartir', - 'market.setupIntro' => 'Compartir con xente cercano ye opcional. Namás indica la to zona averada — yá tas coneutáu a servidores comunitarios compartíos pa qu\'otres persones atopen lo qu\'ufiertes, ensin nenguna empresa en mediu.', + 'market.setupIntro' => 'Compartir con xente cercano ye opcional. Namás indica la to zona averada — lo qu\'ufiertes viaxa per servidores comunitarios, calteníos por persones y coleutivos, non por una empresa, pa qu\'otres persones cercanes puedan atopalo.', 'market.areaLabel' => 'La to zona', 'market.areaHelp' => 'Caltiénse averao a costafecha — la to zona, enxamás un puntu esactu.', 'market.areaSet' => 'La to zona ta puesta — averada, enxamás el to puntu esactu', diff --git a/apps/app_seeds/lib/i18n/strings_de.g.dart b/apps/app_seeds/lib/i18n/strings_de.g.dart index 3db7388..20e6a89 100644 --- a/apps/app_seeds/lib/i18n/strings_de.g.dart +++ b/apps/app_seeds/lib/i18n/strings_de.g.dart @@ -769,7 +769,7 @@ class _Translations$market$de extends Translations$market$en { @override String get contact => 'Nachricht'; @override String get mine => 'Du'; @override String get configTitle => 'Teilen-Einrichtung'; - @override String get setupIntro => 'Teilen mit Menschen in der Nähe ist optional. Gib einfach dein ungefähres Gebiet an - du bist bereits mit freigegebenen Gemeinschaftsservern verbunden, damit Leute das finden können, das du anbietest, ohne ein Unternehmen dazwischen.'; + @override String get setupIntro => 'Teilen mit Menschen in der Nähe ist optional. Gib einfach dein ungefähres Gebiet an - was du anbietest, läuft über Gemeinschaftsserver, die von Menschen und Kollektiven betrieben werden, nicht von einem Unternehmen, damit andere in deiner Nähe es finden können.'; @override String get areaLabel => 'Dein Gebiet'; @override String get areaHelp => 'Absichtlich grob gehalten - deine Zone, nie ein genauer Punkt.'; @override String get areaSet => 'Dein Gebiet ist gesetzt - grob, nie dein genauer Punkt'; @@ -1801,7 +1801,7 @@ extension on TranslationsDe { 'market.contact' => 'Nachricht', 'market.mine' => 'Du', 'market.configTitle' => 'Teilen-Einrichtung', - 'market.setupIntro' => 'Teilen mit Menschen in der Nähe ist optional. Gib einfach dein ungefähres Gebiet an - du bist bereits mit freigegebenen Gemeinschaftsservern verbunden, damit Leute das finden können, das du anbietest, ohne ein Unternehmen dazwischen.', + 'market.setupIntro' => 'Teilen mit Menschen in der Nähe ist optional. Gib einfach dein ungefähres Gebiet an - was du anbietest, läuft über Gemeinschaftsserver, die von Menschen und Kollektiven betrieben werden, nicht von einem Unternehmen, damit andere in deiner Nähe es finden können.', 'market.areaLabel' => 'Dein Gebiet', 'market.areaHelp' => 'Absichtlich grob gehalten - deine Zone, nie ein genauer Punkt.', 'market.areaSet' => 'Dein Gebiet ist gesetzt - grob, nie dein genauer Punkt', diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index c74125c..c1d1e8a 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -1483,8 +1483,8 @@ class Translations$market$en { /// en: 'Sharing setup' String get configTitle => 'Sharing setup'; - /// en: 'Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle.' - String get setupIntro => 'Sharing with people nearby is optional. Just set your rough area — you\'re already connected to shared community servers so people can find what you offer, with no company in the middle.'; + /// en: 'Sharing with people nearby is optional. Just set your rough area — what you offer travels through community servers, run by people and collectives rather than a company, so others nearby can find it.' + String get setupIntro => 'Sharing with people nearby is optional. Just set your rough area — what you offer travels through community servers, run by people and collectives rather than a company, so others nearby can find it.'; /// en: 'Your area' String get areaLabel => 'Your area'; @@ -3186,7 +3186,7 @@ extension on Translations { 'market.contact' => 'Message', 'market.mine' => 'You', 'market.configTitle' => 'Sharing setup', - 'market.setupIntro' => 'Sharing with people nearby is optional. Just set your rough area — you\'re already connected to shared community servers so people can find what you offer, with no company in the middle.', + 'market.setupIntro' => 'Sharing with people nearby is optional. Just set your rough area — what you offer travels through community servers, run by people and collectives rather than a company, so others nearby can find it.', 'market.areaLabel' => 'Your area', 'market.areaHelp' => 'Kept rough on purpose — your zone, never an exact spot.', 'market.areaSet' => 'Your area is set — kept coarse, never your exact spot', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index 947c10e..3790e39 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -807,7 +807,7 @@ class _Translations$market$es extends Translations$market$en { @override String get contact => 'Mensaje'; @override String get mine => 'Tú'; @override String get configTitle => 'Configuración de compartir'; - @override String get setupIntro => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.'; + @override String get setupIntro => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — lo que ofreces viaja por servidores comunitarios, mantenidos por personas y colectivos, no por una empresa, para que otras personas cercanas puedan encontrarlo.'; @override String get areaLabel => 'Tu zona'; @override String get areaHelp => 'Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.'; @override String get areaSet => 'Tu zona está puesta — aproximada, nunca tu punto exacto'; @@ -1924,7 +1924,7 @@ extension on TranslationsEs { 'market.contact' => 'Mensaje', 'market.mine' => 'Tú', 'market.configTitle' => 'Configuración de compartir', - 'market.setupIntro' => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.', + 'market.setupIntro' => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — lo que ofreces viaja por servidores comunitarios, mantenidos por personas y colectivos, no por una empresa, para que otras personas cercanas puedan encontrarlo.', 'market.areaLabel' => 'Tu zona', 'market.areaHelp' => 'Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.', 'market.areaSet' => 'Tu zona está puesta — aproximada, nunca tu punto exacto', diff --git a/apps/app_seeds/lib/i18n/strings_fr.g.dart b/apps/app_seeds/lib/i18n/strings_fr.g.dart index 38156f9..9575732 100644 --- a/apps/app_seeds/lib/i18n/strings_fr.g.dart +++ b/apps/app_seeds/lib/i18n/strings_fr.g.dart @@ -769,7 +769,7 @@ class _Translations$market$fr extends Translations$market$en { @override String get contact => 'Message'; @override String get mine => 'Vous'; @override String get configTitle => 'Configuration du partage'; - @override String get setupIntro => 'Le partage avec les gens à proximité est optionnel. Définissez simplement votre zone approximative — vous êtes déjà connectés aux serveurs communautaires partagés pour que les gens trouvent ce que vous offrez, sans aucune entreprise au milieu.'; + @override String get setupIntro => 'Le partage avec les gens à proximité est optionnel. Définissez simplement votre zone approximative — ce que vous offrez passe par des serveurs communautaires, tenus par des personnes et des collectifs et non par une entreprise, pour que d\'autres près de chez vous puissent le trouver.'; @override String get areaLabel => 'Votre zone'; @override String get areaHelp => 'Gardée approximative volontairement — votre zone, jamais un point exact.'; @override String get areaSet => 'Votre zone est définie — approximative, jamais votre point exact'; @@ -1801,7 +1801,7 @@ extension on TranslationsFr { 'market.contact' => 'Message', 'market.mine' => 'Vous', 'market.configTitle' => 'Configuration du partage', - 'market.setupIntro' => 'Le partage avec les gens à proximité est optionnel. Définissez simplement votre zone approximative — vous êtes déjà connectés aux serveurs communautaires partagés pour que les gens trouvent ce que vous offrez, sans aucune entreprise au milieu.', + 'market.setupIntro' => 'Le partage avec les gens à proximité est optionnel. Définissez simplement votre zone approximative — ce que vous offrez passe par des serveurs communautaires, tenus par des personnes et des collectifs et non par une entreprise, pour que d\'autres près de chez vous puissent le trouver.', 'market.areaLabel' => 'Votre zone', 'market.areaHelp' => 'Gardée approximative volontairement — votre zone, jamais un point exact.', 'market.areaSet' => 'Votre zone est définie — approximative, jamais votre point exact', diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index 48d5799..d4aae83 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -808,7 +808,7 @@ class Translations$market$pt extends Translations$market$en { @override String get contact => 'Mensagem'; @override String get mine => 'Tu'; @override String get configTitle => 'Configuração de partilha'; - @override String get setupIntro => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.'; + @override String get setupIntro => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — o que ofereces viaja por servidores comunitários, mantidos por pessoas e coletivos, não por uma empresa, para que outras pessoas perto de ti o possam encontrar.'; @override String get areaLabel => 'A tua zona'; @override String get areaHelp => 'Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.'; @override String get areaSet => 'A tua zona está definida — aproximada, nunca o teu ponto exato'; @@ -1926,7 +1926,7 @@ extension on TranslationsPt { 'market.contact' => 'Mensagem', 'market.mine' => 'Tu', 'market.configTitle' => 'Configuração de partilha', - 'market.setupIntro' => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.', + 'market.setupIntro' => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — o que ofereces viaja por servidores comunitários, mantidos por pessoas e coletivos, não por uma empresa, para que outras pessoas perto de ti o possam encontrar.', 'market.areaLabel' => 'A tua zona', 'market.areaHelp' => 'Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.', 'market.areaSet' => 'A tua zona está definida — aproximada, nunca o teu ponto exato', diff --git a/apps/app_seeds/lib/i18n/strings_pt_BR.g.dart b/apps/app_seeds/lib/i18n/strings_pt_BR.g.dart index b2bdb8a..0d51e10 100644 --- a/apps/app_seeds/lib/i18n/strings_pt_BR.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt_BR.g.dart @@ -809,7 +809,7 @@ class _Translations$market$pt_BR extends Translations$market$pt { @override String get contact => 'Mensagem'; @override String get mine => 'Você'; @override String get configTitle => 'Configuração de compartilha'; - @override String get setupIntro => 'Compartilhar com pessoas por perto é opcional. Basta indicar a sua zona aproximada — já está conectado a servidores comunitários compartilhados para que outras pessoas encontrem o que você oferece, sem nenhuma empresa no meio.'; + @override String get setupIntro => 'Compartilhar com pessoas por perto é opcional. Basta indicar a sua zona aproximada — o que você oferece viaja por servidores comunitários, mantidos por pessoas e coletivos, não por uma empresa, para que outras pessoas perto de você possam encontrar.'; @override String get areaLabel => 'A sua zona'; @override String get areaHelp => 'Mantém-se aproximado de propósito — a sua zona, nunca um ponto exato.'; @override String get areaSet => 'A sua zona está definida — aproximada, nunca o seu ponto exato'; @@ -1927,7 +1927,7 @@ extension on TranslationsPtBr { 'market.contact' => 'Mensagem', 'market.mine' => 'Você', 'market.configTitle' => 'Configuração de compartilha', - 'market.setupIntro' => 'Compartilhar com pessoas por perto é opcional. Basta indicar a sua zona aproximada — já está conectado a servidores comunitários compartilhados para que outras pessoas encontrem o que você oferece, sem nenhuma empresa no meio.', + 'market.setupIntro' => 'Compartilhar com pessoas por perto é opcional. Basta indicar a sua zona aproximada — o que você oferece viaja por servidores comunitários, mantidos por pessoas e coletivos, não por uma empresa, para que outras pessoas perto de você possam encontrar.', 'market.areaLabel' => 'A sua zona', 'market.areaHelp' => 'Mantém-se aproximado de propósito — a sua zona, nunca um ponto exato.', 'market.areaSet' => 'A sua zona está definida — aproximada, nunca o seu ponto exato', From 2c04f4bfb6779c7e3750c3aedd4b6a7db424c2cf Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 13:06:56 +0200 Subject: [PATCH 7/8] =?UTF-8?q?fix(site):=20honest=20decentralization=20co?= =?UTF-8?q?py=20=E2=80=94=20community=20servers,=20not=20'no=20server'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The landing claimed seeds spread 'with no server in the middle' and the About page said people communicate 'directly with each other', while sharing in fact travels through community relays. Reworded (en/es/pt, docs sources + generated about pages via build-legal.sh): what you share travels through community servers run by people and collectives — many of them, anyone can add one, none a controllable center. --- docs/o-que-e-tane.md | 7 ++++--- docs/que-es-tane.md | 7 ++++--- docs/what-is-tane.md | 7 ++++--- site/content/_index.en.md | 2 +- site/content/_index.es.md | 2 +- site/content/_index.pt.md | 2 +- site/content/about.en.md | 7 ++++--- site/content/about.es.md | 7 ++++--- site/content/about.pt.md | 7 ++++--- 9 files changed, 27 insertions(+), 21 deletions(-) diff --git a/docs/o-que-e-tane.md b/docs/o-que-e-tane.md index 17aee0f..bb886ed 100644 --- a/docs/o-que-e-tane.md +++ b/docs/o-que-e-tane.md @@ -39,9 +39,10 @@ com cadernos e folhas de cálculo. - **Os teus dados são teus.** A tua informação fica guardada no **teu próprio telemóvel**, protegida com uma chave, não nos computadores de nenhuma empresa. Não a vendemos nem a colocamos em lado nenhum. Sem publicidade. - **Ninguém o pode desligar ou controlar.** Não há nenhum computador central por onde tudo passa: - as pessoas comunicam **diretamente entre si**, como quem passa sementes de mão em mão. Assim, - ninguém o pode censurar nem cobrar para o usares (é isso que significa ser - **descentralizado**). + o que partilhas viaja por **servidores da comunidade**, mantidos por pessoas e coletivos — + há vários e qualquer pessoa pode acrescentar o seu — pelo que nenhum deles é um centro + a partir do qual censurar ou cobrar para o usares, como quem passa sementes de mão em mão + (é isso que significa ser **descentralizado**). - **Confias de boca em boca.** Lidas com pessoas que conheces e com as que são avalizadas por pessoas em quem confias — como sempre funcionou numa aldeia ou numa feira — sem teres de dar o teu nome verdadeiro ou o teu telefone a não ser que queiras. diff --git a/docs/que-es-tane.md b/docs/que-es-tane.md index 61d8b53..3dc5c3e 100644 --- a/docs/que-es-tane.md +++ b/docs/que-es-tane.md @@ -41,9 +41,10 @@ con libretas y hojas de cálculo. clave, no en los ordenadores de una empresa. No la vendemos ni la subimos a ningún sitio. Sin publicidad. - **Nadie la puede apagar ni controlar.** No hay un ordenador central por el que pase todo: - las personas se comunican **directamente entre sí**, como quien se pasa unas semillas de - mano en mano. Por eso nadie puede censurarla ni cobrarte por usarla (eso es que sea - **descentralizada**). + lo que compartes viaja por **servidores de la comunidad**, mantenidos por personas y + colectivos — hay varios y cualquiera puede añadir el suyo — así que ninguno es un centro + desde el que censurarla o cobrarte por usarla, como quien se pasa unas semillas de mano + en mano (eso es que sea **descentralizada**). - **Te fías por el boca a boca.** Te relacionas con gente que conoces y con quien te recomienda gente de confianza —como siempre se ha hecho en un pueblo o en una feria— sin tener que dar tu nombre real ni tu teléfono si no quieres. diff --git a/docs/what-is-tane.md b/docs/what-is-tane.md index a378f0f..9d3ce9c 100644 --- a/docs/what-is-tane.md +++ b/docs/what-is-tane.md @@ -39,9 +39,10 @@ still do it with notebooks and spreadsheets. - **Your data is yours.** Your information is kept on **your own phone**, protected with a key, not on some company's computers. We don't sell it or upload it anywhere. No ads. - **No one can shut it down or control it.** There's no central computer that everything - passes through: people communicate **directly with each other**, like passing seeds from - hand to hand. So no one can censor it or charge you to use it (that's what being - **decentralized** means). + passes through: what you share travels over **community servers**, run by people and + collectives — there are many, and anyone can add their own — so none of them is a center + that could censor it or charge you to use it, like passing seeds from hand to hand + (that's what being **decentralized** means). - **You trust by word of mouth.** You deal with people you know and with those vouched for by people you trust — the way it's always worked in a village or at a fair — without having to give your real name or phone number unless you want to. diff --git a/site/content/_index.en.md b/site/content/_index.en.md index 7889ac6..1212621 100644 --- a/site/content/_index.en.md +++ b/site/content/_index.en.md @@ -57,7 +57,7 @@ values: points: - "No center: no company can shut it down, censor it, or be fined for it." - "Anti-monopoly by design — sharing seeds multiplies the commons instead of depleting it." - - "Spreads like a seed, person to person, with no server in the middle." + - "Spreads like a seed, person to person; what you share travels through community servers anyone can run — not a company's central server." - "Free software (AGPL-3.0). No ads, no commissions, no business model." --- diff --git a/site/content/_index.es.md b/site/content/_index.es.md index 31043d7..9cf3285 100644 --- a/site/content/_index.es.md +++ b/site/content/_index.es.md @@ -57,7 +57,7 @@ values: points: - "Sin centro: ninguna empresa puede apagarlo, censurarlo ni ser multada por ello." - "Anti-monopolio por diseño: compartir semillas multiplica el común en lugar de agotarlo." - - "Se propaga como una semilla, de persona a persona, sin servidor en medio." + - "Se propaga como una semilla, de persona a persona; lo que compartes viaja por servidores comunitarios que cualquiera puede montar — no por el servidor central de una empresa." - "Software libre (AGPL-3.0). Sin anuncios, sin comisiones, sin modelo de negocio." --- diff --git a/site/content/_index.pt.md b/site/content/_index.pt.md index a2bd42a..1dedc44 100644 --- a/site/content/_index.pt.md +++ b/site/content/_index.pt.md @@ -57,7 +57,7 @@ values: points: - "Sem centro: nenhuma empresa o pode desligar, censurar ou multar por isso." - "Anti-monopólio por desenho — partilhar sementes multiplica os comuns em vez de os esgotar." - - "Espalha-se como uma semente, de pessoa a pessoa, sem servidor pelo meio." + - "Espalha-se como uma semente, de pessoa a pessoa; o que partilhas viaja por servidores comunitários que qualquer pessoa pode montar — não pelo servidor central de uma empresa." - "Software livre (AGPL-3.0). Sem publicidade, sem comissões, sem modelo de negócio." --- diff --git a/site/content/about.en.md b/site/content/about.en.md index c87f780..dd404d3 100644 --- a/site/content/about.en.md +++ b/site/content/about.en.md @@ -45,9 +45,10 @@ still do it with notebooks and spreadsheets. - **Your data is yours.** Your information is kept on **your own phone**, protected with a key, not on some company's computers. We don't sell it or upload it anywhere. No ads. - **No one can shut it down or control it.** There's no central computer that everything - passes through: people communicate **directly with each other**, like passing seeds from - hand to hand. So no one can censor it or charge you to use it (that's what being - **decentralized** means). + passes through: what you share travels over **community servers**, run by people and + collectives — there are many, and anyone can add their own — so none of them is a center + that could censor it or charge you to use it, like passing seeds from hand to hand + (that's what being **decentralized** means). - **You trust by word of mouth.** You deal with people you know and with those vouched for by people you trust — the way it's always worked in a village or at a fair — without having to give your real name or phone number unless you want to. diff --git a/site/content/about.es.md b/site/content/about.es.md index 09e06e6..9de300c 100644 --- a/site/content/about.es.md +++ b/site/content/about.es.md @@ -47,9 +47,10 @@ con libretas y hojas de cálculo. clave, no en los ordenadores de una empresa. No la vendemos ni la subimos a ningún sitio. Sin publicidad. - **Nadie la puede apagar ni controlar.** No hay un ordenador central por el que pase todo: - las personas se comunican **directamente entre sí**, como quien se pasa unas semillas de - mano en mano. Por eso nadie puede censurarla ni cobrarte por usarla (eso es que sea - **descentralizada**). + lo que compartes viaja por **servidores de la comunidad**, mantenidos por personas y + colectivos — hay varios y cualquiera puede añadir el suyo — así que ninguno es un centro + desde el que censurarla o cobrarte por usarla, como quien se pasa unas semillas de mano + en mano (eso es que sea **descentralizada**). - **Te fías por el boca a boca.** Te relacionas con gente que conoces y con quien te recomienda gente de confianza —como siempre se ha hecho en un pueblo o en una feria— sin tener que dar tu nombre real ni tu teléfono si no quieres. diff --git a/site/content/about.pt.md b/site/content/about.pt.md index 2100399..307a876 100644 --- a/site/content/about.pt.md +++ b/site/content/about.pt.md @@ -45,9 +45,10 @@ com cadernos e folhas de cálculo. - **Os teus dados são teus.** A tua informação fica guardada no **teu próprio telemóvel**, protegida com uma chave, não nos computadores de nenhuma empresa. Não a vendemos nem a colocamos em lado nenhum. Sem publicidade. - **Ninguém o pode desligar ou controlar.** Não há nenhum computador central por onde tudo passa: - as pessoas comunicam **diretamente entre si**, como quem passa sementes de mão em mão. Assim, - ninguém o pode censurar nem cobrar para o usares (é isso que significa ser - **descentralizado**). + o que partilhas viaja por **servidores da comunidade**, mantidos por pessoas e coletivos — + há vários e qualquer pessoa pode acrescentar o seu — pelo que nenhum deles é um centro + a partir do qual censurar ou cobrar para o usares, como quem passa sementes de mão em mão + (é isso que significa ser **descentralizado**). - **Confias de boca em boca.** Lidas com pessoas que conheces e com as que são avalizadas por pessoas em quem confias — como sempre funcionou numa aldeia ou numa feira — sem teres de dar o teu nome verdadeiro ou o teu telefone a não ser que queiras. From 05e4689339029b62d3fbed7b5ce9e0dd9ce19341 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 16:03:07 +0200 Subject: [PATCH 8/8] =?UTF-8?q?chore(release):=20v0.1.12=20=E2=80=94=20fre?= =?UTF-8?q?sh-install=20market=20connectivity=20+=20honest=20server=20copy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/app_seeds/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app_seeds/pubspec.yaml b/apps/app_seeds/pubspec.yaml index 98b13c1..ff8d8d0 100644 --- a/apps/app_seeds/pubspec.yaml +++ b/apps/app_seeds/pubspec.yaml @@ -1,7 +1,7 @@ name: tane description: "Tane — local-first, encrypted, decentralized traditional-seed inventory and market." publish_to: 'none' -version: 0.1.11+13 +version: 0.1.12+14 environment: sdk: ^3.11.5