diff --git a/apps/app_seeds/lib/i18n/ast.i18n.json b/apps/app_seeds/lib/i18n/ast.i18n.json index 7b1a054..f27fa57 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 — lo qu'ufiertes viaxa per servidores comunitarios, calteníos por persones y coleutivos, non por una empresa, pa qu'otres persones cercanes puedan atopalo.", + "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.", "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 d6230c7..6d63d63 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 - 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.", + "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.", "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 e1caf31..791f12d 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 — what you offer travels through community servers, run by people and collectives rather than a company, so others nearby can find it.", + "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.", "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 5b1a570..d1faa15 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 — lo que ofreces viaja por servidores comunitarios, mantenidos por personas y colectivos, no por una empresa, para que otras personas cercanas puedan encontrarlo.", + "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.", "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 8b5b4b8..b56e42c 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 — 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.", + "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.", "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 810b0d4..2e78828 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 — 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.", + "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.", "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 bc4edba..4e9c920 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 — 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.", + "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.", "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 0132a97..6acb839 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-22 at 11:02 UTC +/// Built on 2026-07-20 at 21:06 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 c3ee9bd..b2a316f 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 — 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 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 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 — 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.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.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 20e6a89..3db7388 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 - 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 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 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 - 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.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.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 c1d1e8a..c74125c 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 — 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: '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: '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 — what you offer travels through community servers, run by people and collectives rather than a company, so others nearby can find it.', + '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.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 3790e39..947c10e 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 — 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 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 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 — lo que ofreces viaja por servidores comunitarios, mantenidos por personas y colectivos, no por una empresa, para que otras personas cercanas puedan encontrarlo.', + '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.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 9575732..38156f9 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 — 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 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 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 — 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.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.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 d4aae83..48d5799 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 — 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 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 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 — 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.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.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 0d51e10..b2bdb8a 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 — 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 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 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 — 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.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.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', diff --git a/apps/app_seeds/lib/services/social_connection.dart b/apps/app_seeds/lib/services/social_connection.dart index 332621c..8e57949 100644 --- a/apps/app_seeds/lib/services/social_connection.dart +++ b/apps/app_seeds/lib/services/social_connection.dart @@ -25,36 +25,19 @@ class SocialConnection { required SocialSettings settings, SessionOpener? open, Stream? online, - List? retrySchedule, }) : _settings = settings, _open = open ?? social.openSession, - _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), - ]; + _online = online; 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; @@ -65,14 +48,10 @@ 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()); } }); @@ -96,40 +75,15 @@ class SocialConnection { return null; } _current = s; - _retryIndex = 0; - _cancelRetry(); _sessions.add(s); return s; } catch (_) { - _scheduleRetry(); // unreachable — retry with backoff (see below) - return null; + return null; // unreachable — a later connectivity change retries } 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; @@ -141,7 +95,6 @@ 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 60e1c36..d6c462b 100644 --- a/apps/app_seeds/lib/state/offers_cubit.dart +++ b/apps/app_seeds/lib/state/offers_cubit.dart @@ -10,7 +10,6 @@ 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. @@ -29,7 +28,6 @@ class OffersState extends Equatable { this.nextPageCursor, this.publishing = false, this.hasSearched = false, - this.connectionEpoch = 0, this.error, }); @@ -82,11 +80,6 @@ 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; @@ -143,7 +136,6 @@ class OffersState extends Equatable { int? Function()? nextPageCursor, bool? publishing, bool? hasSearched, - int? connectionEpoch, String? Function()? error, }) { return OffersState( @@ -161,7 +153,6 @@ 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, ); } @@ -181,7 +172,6 @@ class OffersState extends Equatable { nextPageCursor, publishing, hasSearched, - connectionEpoch, error, ]; } @@ -193,40 +183,15 @@ 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()) { - // 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); - } + super(const OffersState()); - 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)); - } - } + final OfferTransport? _transport; /// Fetches a variety's cover photo bytes; null in tests or when no inventory /// repo is wired. @@ -260,7 +225,6 @@ 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)); @@ -503,7 +467,6 @@ class OffersCubit extends Cubit { @override Future close() async { _searchTimeout?.cancel(); - await _connSub?.cancel(); await _sub?.cancel(); await _onDispose?.call(); return super.close(); @@ -521,7 +484,6 @@ 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 b172ca2..23941a5 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(); @@ -122,10 +122,10 @@ class _MarketScreenState extends State { _loading = false; }); await previous?.close(); - if (area.isNotEmpty) { + if (area.isNotEmpty && cubit.isOnline) { // Flush anything parked while offline, then show what's out there. final outbox = widget.outbox; - if (outbox != null && repo != null && cubit.isOnline) { + if (outbox != null && repo != null) { await flushOutbox( outbox: outbox, cubit: cubit, @@ -136,8 +136,6 @@ 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)); } } @@ -208,18 +206,12 @@ 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 @@ -248,13 +240,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( @@ -306,17 +298,6 @@ 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". @@ -328,6 +309,15 @@ 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( @@ -335,10 +325,8 @@ 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)), ], ), ); @@ -382,22 +370,16 @@ 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), @@ -449,17 +431,13 @@ 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]; @@ -596,9 +574,7 @@ 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), @@ -622,10 +598,8 @@ 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), @@ -633,8 +607,7 @@ 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, @@ -933,226 +906,194 @@ class _ConfigSheetState extends State<_ConfigSheet> { Widget build(BuildContext context) { final t = context.t; final hasArea = _area.text.trim().isNotEmpty; - // 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, - ), + 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: 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), ), - 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, - ), - ), - ), - const SizedBox(height: 12), - Row( - children: [ - Icon( - hasArea ? Icons.check_circle : Icons.pending_outlined, - size: 18, - color: hasArea ? seedGreen : seedMuted, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - hasArea ? t.market.areaSet : t.market.areaNotSet, - style: TextStyle( - color: hasArea ? seedOnSurface : seedMuted, - fontSize: 13, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - Align( - alignment: AlignmentDirectional.centerStart, + if (_locationError != null) + Padding( + padding: const EdgeInsets.only(top: 6), child: Text( - t.market.rangeLabel, + _locationError!, + style: const TextStyle( + color: Color(0xFFB3261E), fontSize: 12), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Icon( + hasArea + ? Icons.check_circle + : Icons.pending_outlined, + size: 18, + color: hasArea ? seedGreen : seedMuted, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + hasArea ? t.market.areaSet : t.market.areaNotSet, + style: TextStyle( + color: hasArea ? seedOnSurface : seedMuted, + fontSize: 13, + ), + ), + ), + ], + ), + 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: 8), - SegmentedButton( - key: const Key('market.range'), - segments: [ - ButtonSegment( - value: 5, - label: Text(t.market.rangeNear), + children: [ + TextField( + key: const Key('market.area'), + controller: _area, + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + labelText: t.market.areaCodeLabel, + helperText: t.market.areaCodeHint, + helperMaxLines: 2, + ), ), - ButtonSegment( - value: 4, - label: Text(t.market.rangeArea), + const SizedBox(height: 14), + Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + t.market.serversLabel, + style: const TextStyle( + fontSize: 13, color: seedMuted), + ), ), - ButtonSegment( - value: 3, - label: Text(t.market.rangeRegion), + 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), + ), ), ], - 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), - ), - ], - ), + ), + const SizedBox(height: 20), + FilledButton( + key: const Key('market.save'), + onPressed: _save, + child: Text(t.market.save), + ), + ], ), - ), + ), ); } } diff --git a/apps/app_seeds/pubspec.yaml b/apps/app_seeds/pubspec.yaml index ff8d8d0..241cb72 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.12+14 +version: 0.1.10+12 environment: sdk: ^3.11.5 diff --git a/apps/app_seeds/test/services/social_connection_test.dart b/apps/app_seeds/test/services/social_connection_test.dart index 7b856de..e4c7151 100644 --- a/apps/app_seeds/test/services/social_connection_test.dart +++ b/apps/app_seeds/test/services/social_connection_test.dart @@ -45,13 +45,11 @@ 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(); @@ -108,69 +106,6 @@ 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 4c84dc6..650088a 100644 --- a/apps/app_seeds/test/state/offers_cubit_test.dart +++ b/apps/app_seeds/test/state/offers_cubit_test.dart @@ -7,11 +7,7 @@ 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'; @@ -839,111 +835,4 @@ 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/apps/app_seeds/test/ui/market_screen_test.dart b/apps/app_seeds/test/ui/market_screen_test.dart index 5f99e18..b063201 100644 --- a/apps/app_seeds/test/ui/market_screen_test.dart +++ b/apps/app_seeds/test/ui/market_screen_test.dart @@ -84,9 +84,8 @@ void main() { findsOneWidget); }); - testWidgets( - 'a fresh install (no area, unreachable) asks for the area first, ' - 'not the connection', (tester) async { + testWidgets('MarketScreen with no relays configured degrades to offline', + (tester) async { final social = await SocialService.fromRootSeedHex('00' * 32); final settings = SocialSettings(InMemorySecretStore()); await settings.setRelayUrls(const []); // offline: don't hit the network @@ -95,23 +94,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Seeds near you'), findsOneWidget); // app bar title - // 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); + expect(find.text('Retry'), findsOneWidget); // can't-reach state offers retry }); testWidgets('the range selector persists the chosen search precision', @@ -165,36 +148,6 @@ 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); diff --git a/docs/o-que-e-tane.md b/docs/o-que-e-tane.md index bb886ed..17aee0f 100644 --- a/docs/o-que-e-tane.md +++ b/docs/o-que-e-tane.md @@ -39,10 +39,9 @@ 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: - 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**). + 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**). - **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 3dc5c3e..61d8b53 100644 --- a/docs/que-es-tane.md +++ b/docs/que-es-tane.md @@ -41,10 +41,9 @@ 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: - 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**). + 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**). - **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 9d3ce9c..a378f0f 100644 --- a/docs/what-is-tane.md +++ b/docs/what-is-tane.md @@ -39,10 +39,9 @@ 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: 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). + 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). - **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/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart b/packages/commons_core/lib/src/social/nostr/nostr_offer_transport.dart index bd4c9d8..305f842 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,10 +46,9 @@ class NostrOfferTransport implements OfferTransport { @override Future discoverPage(DiscoveryQuery query) async { - // 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))] + 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. ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); final offers = []; for (final e in events) { 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 aad754d..eac038d 100644 --- a/packages/commons_core/lib/src/social/nostr/relay_pool.dart +++ b/packages/commons_core/lib/src/social/nostr/relay_pool.dart @@ -26,32 +26,19 @@ 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 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". + /// Connects to each of [relayUrls], skipping any that fail. 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 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); - }, - ), + await NostrConnection.connect(url, identity: identity), ); } 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 cdb41bc..52de12f 100644 --- a/packages/commons_core/test/social/relay_pool_test.dart +++ b/packages/commons_core/test/social/relay_pool_test.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:typed_data'; import 'package:commons_core/commons_core.dart'; @@ -59,38 +58,6 @@ 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( diff --git a/site/assets/css/main.css b/site/assets/css/main.css index 55e230f..9cb1bc9 100644 --- a/site/assets/css/main.css +++ b/site/assets/css/main.css @@ -121,15 +121,11 @@ a { color: var(--green); } .lang-menu a.lang:hover { background: var(--container); } .lang-menu span.current { color: var(--muted); font-weight: 600; } -/* 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. */ +/* Narrow phones: brand on its own row, nav wraps beneath it, left-aligned. */ @media (max-width: 560px) { .site-header { gap: .5rem; } .brand { font-size: 1.15rem; } - .site-nav { gap: .4rem 1rem; font-size: .95rem; } + .site-nav { width: 100%; gap: .4rem 1rem; font-size: .95rem; } } main { max-width: var(--maxw); margin: 0 auto; padding: 0 clamp(1rem, 4vw, 2rem); } diff --git a/site/content/_index.en.md b/site/content/_index.en.md index 1212621..7889ac6 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; what you share travels through community servers anyone can run — not a company's central server." + - "Spreads like a seed, person to person, with no server in the middle." - "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 9cf3285..31043d7 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; lo que compartes viaja por servidores comunitarios que cualquiera puede montar — no por el servidor central de una empresa." + - "Se propaga como una semilla, de persona a persona, sin servidor en medio." - "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 1dedc44..a2bd42a 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; o que partilhas viaja por servidores comunitários que qualquer pessoa pode montar — não pelo servidor central de uma empresa." + - "Espalha-se como uma semente, de pessoa a pessoa, sem servidor pelo meio." - "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 dd404d3..c87f780 100644 --- a/site/content/about.en.md +++ b/site/content/about.en.md @@ -45,10 +45,9 @@ 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: 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). + 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). - **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 9de300c..09e06e6 100644 --- a/site/content/about.es.md +++ b/site/content/about.es.md @@ -47,10 +47,9 @@ 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: - 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**). + 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**). - **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 307a876..2100399 100644 --- a/site/content/about.pt.md +++ b/site/content/about.pt.md @@ -45,10 +45,9 @@ 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: - 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**). + 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**). - **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.