From fef174e8c4f0c215e6d7f821952529552b1caa76 Mon Sep 17 00:00:00 2001 From: vjrj Date: Sat, 11 Jul 2026 13:03:20 +0200 Subject: [PATCH] refactor(trust): ego-centric trust replaces the global Duniter membership MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The global membership rule (curated bootstrap referents + sigQty/stepMax parameters) solved sybil-proof identity for a UBI — a problem this app doesn't have — and its screen leaked graph jargon (npub roots, parameter steppers). Trust is now computed from the user's own position only: you vouch / vouched by people you know (distance <=2) / vouched by N. - TrustCubit: drop networkMember tier, referents and params; keep the circle rule and the 365-day vouch expiry (renewable, self-pruning). - Delete TrustReferents, WotSettings, TrustNetworkScreen and the bundled referents asset; unwire injector/bootstrap/app/chat. - New 'Your people' screen (/your-people, from the profile): who you vouch for (revocable) and who vouches for you, names via ProfileCache. - i18n: wot.* removed, yourPeople.* added (en/es/pt/ast); trust.member removed. Kind 30777 events on relays stay fully compatible — only the interpretation changes. --- apps/app_seeds/assets/trust/referents.json | 5 - apps/app_seeds/lib/app.dart | 30 +-- apps/app_seeds/lib/bootstrap.dart | 4 - apps/app_seeds/lib/di/injector.dart | 5 - apps/app_seeds/lib/i18n/ast.i18n.json | 33 +-- apps/app_seeds/lib/i18n/en.i18n.json | 33 +-- apps/app_seeds/lib/i18n/es.i18n.json | 33 +-- apps/app_seeds/lib/i18n/pt.i18n.json | 33 +-- apps/app_seeds/lib/i18n/strings.g.dart | 4 +- apps/app_seeds/lib/i18n/strings_ast.g.dart | 66 ++--- apps/app_seeds/lib/i18n/strings_en.g.dart | 106 +++----- apps/app_seeds/lib/i18n/strings_es.g.dart | 66 ++--- apps/app_seeds/lib/i18n/strings_pt.g.dart | 66 ++--- .../lib/services/trust_referents.dart | 73 ------ apps/app_seeds/lib/services/wot_settings.dart | 45 ---- apps/app_seeds/lib/state/trust_cubit.dart | 70 ++--- apps/app_seeds/lib/ui/chat_screen.dart | 17 +- apps/app_seeds/lib/ui/profile_screen.dart | 17 +- .../lib/ui/trust_network_screen.dart | 228 ----------------- apps/app_seeds/lib/ui/your_people_screen.dart | 240 ++++++++++++++++++ apps/app_seeds/pubspec.yaml | 3 - .../test/services/trust_referents_test.dart | 45 ---- .../test/services/wot_settings_test.dart | 28 -- .../test/state/trust_cubit_test.dart | 34 +-- .../test/ui/your_people_screen_test.dart | 174 +++++++++++++ 25 files changed, 595 insertions(+), 863 deletions(-) delete mode 100644 apps/app_seeds/assets/trust/referents.json delete mode 100644 apps/app_seeds/lib/services/trust_referents.dart delete mode 100644 apps/app_seeds/lib/services/wot_settings.dart delete mode 100644 apps/app_seeds/lib/ui/trust_network_screen.dart create mode 100644 apps/app_seeds/lib/ui/your_people_screen.dart delete mode 100644 apps/app_seeds/test/services/trust_referents_test.dart delete mode 100644 apps/app_seeds/test/services/wot_settings_test.dart create mode 100644 apps/app_seeds/test/ui/your_people_screen_test.dart diff --git a/apps/app_seeds/assets/trust/referents.json b/apps/app_seeds/assets/trust/referents.json deleted file mode 100644 index 0de2d07..0000000 --- a/apps/app_seeds/assets/trust/referents.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "_comment": "Web-of-trust bootstrap referents (Duniter 'seeds'): the founding identities every membership calculation trusts by construction. Fill 'referents' with npub (or 64-char hex) public keys of real founding members — e.g. Ğ1 seed-group organisers running Tanemaki. Empty by design until curated; users can also add their own referents in-app. Locale-agnostic.", - "version": 1, - "referents": [] -} diff --git a/apps/app_seeds/lib/app.dart b/apps/app_seeds/lib/app.dart index 4d58c91..5e7f767 100644 --- a/apps/app_seeds/lib/app.dart +++ b/apps/app_seeds/lib/app.dart @@ -21,8 +21,6 @@ import 'services/social_account_store.dart'; import 'services/social_connection.dart'; import 'services/social_service.dart'; import 'services/social_settings.dart'; -import 'services/trust_referents.dart'; -import 'services/wot_settings.dart'; import 'state/inventory_cubit.dart'; import 'state/variety_detail_cubit.dart'; import 'ui/about_screen.dart'; @@ -41,8 +39,8 @@ import 'ui/offline_banner.dart'; import 'ui/profile_screen.dart'; import 'ui/settings_screen.dart'; import 'ui/theme.dart'; -import 'ui/trust_network_screen.dart'; import 'ui/variety_detail_screen.dart'; +import 'ui/your_people_screen.dart'; /// Root widget. Provides the repositories to the tree and wires go_router: /// `/` is the home menu, `/inventory` the list, `/variety/:id` the item detail. @@ -61,8 +59,6 @@ class TaneApp extends StatelessWidget { this.profileStore, this.profileCache, this.socialAccounts, - this.trustReferents, - this.wotSettings, this.inbox, this.notifications, this.showIntro = false, @@ -81,8 +77,6 @@ class TaneApp extends StatelessWidget { profileStore, profileCache, socialAccounts, - trustReferents, - wotSettings, inbox, ) { // A tapped message notification opens that peer's chat. Wired here because @@ -121,10 +115,6 @@ class TaneApp extends StatelessWidget { /// Optional store of the active social identity, for the profile switcher. final SocialAccountStore? socialAccounts; - /// Web-of-trust bootstrap referents + parameters (network membership). - final TrustReferents? trustReferents; - final WotSettings? wotSettings; - /// App-wide inbox listener; drives the messages list's live refresh. final InboxService? inbox; @@ -151,8 +141,6 @@ class TaneApp extends StatelessWidget { ProfileStore? profileStore, ProfileCache? profileCache, SocialAccountStore? socialAccounts, - TrustReferents? trustReferents, - WotSettings? wotSettings, InboxService? inbox, ) { return GoRouter( @@ -208,16 +196,16 @@ class TaneApp extends StatelessWidget { connection: connection, profileStore: profileStore, accounts: socialAccounts, - trustNetworkEnabled: - trustReferents != null && wotSettings != null, + yourPeopleEnabled: true, ), ), - if (trustReferents != null && wotSettings != null) + if (social != null && connection != null) GoRoute( - path: '/trust', - builder: (context, state) => TrustNetworkScreen( - referents: trustReferents, - wotSettings: wotSettings, + path: '/your-people', + builder: (context, state) => YourPeopleScreen( + social: social, + connection: connection, + profileCache: profileCache, ), ), if (social != null && connection != null) @@ -229,8 +217,6 @@ class TaneApp extends StatelessWidget { peerPubkey: state.pathParameters['pubkey']!, messageStore: messageStore, profileCache: profileCache, - trustReferents: trustReferents, - wotSettings: wotSettings, ), ), GoRoute( diff --git a/apps/app_seeds/lib/bootstrap.dart b/apps/app_seeds/lib/bootstrap.dart index 9df9c4e..b2cfca0 100644 --- a/apps/app_seeds/lib/bootstrap.dart +++ b/apps/app_seeds/lib/bootstrap.dart @@ -23,8 +23,6 @@ import 'services/social_connection.dart'; import 'services/social_service.dart'; import 'services/social_settings.dart'; import 'services/sync_service.dart'; -import 'services/trust_referents.dart'; -import 'services/wot_settings.dart'; import 'ui/theme.dart'; /// Boots the app WITHOUT blocking the first frame: paints a splash immediately, @@ -88,8 +86,6 @@ class _BootstrapState extends State { profileStore: getIt(), profileCache: getIt(), socialAccounts: getIt(), - trustReferents: getIt(), - wotSettings: getIt(), inbox: inbox, notifications: notifications, showIntro: !await onboarding.introSeen(), diff --git a/apps/app_seeds/lib/di/injector.dart b/apps/app_seeds/lib/di/injector.dart index 7a8209c..b2b7285 100644 --- a/apps/app_seeds/lib/di/injector.dart +++ b/apps/app_seeds/lib/di/injector.dart @@ -43,9 +43,7 @@ import '../services/social_connection.dart'; import '../services/social_service.dart'; import '../services/social_settings.dart'; import '../services/sync_service.dart'; -import '../services/trust_referents.dart'; import '../services/unread_service.dart'; -import '../services/wot_settings.dart'; /// The app's service locator. Kept to the composition root — widgets get their /// repositories from here (or via BlocProvider), never by reaching into it deep @@ -170,9 +168,6 @@ Future configureDependencies() async { ..registerSingleton(LocaleStore(secretStore)) ..registerSingleton(SocialSettings(secretStore)) ..registerSingleton(accounts) - // Web of trust: network-wide (not per-identity), so registered once. - ..registerSingleton(TrustReferents(secretStore)) - ..registerSingleton(WotSettings(secretStore)) ..registerSingleton(OfferOutbox(secretStore)) // Per-identity stores are namespaced by the active account's scope. ..registerSingleton( diff --git a/apps/app_seeds/lib/i18n/ast.i18n.json b/apps/app_seeds/lib/i18n/ast.i18n.json index 2391428..7b6f353 100644 --- a/apps/app_seeds/lib/i18n/ast.i18n.json +++ b/apps/app_seeds/lib/i18n/ast.i18n.json @@ -537,29 +537,18 @@ "count": "Avalada por {n}", "vouch": "Conozo a esta persona", "vouched": "Avales a esta persona", - "circle": "Nel to círculu", - "member": "Miembru de confianza de la rede" + "circle": "Nel to círculu" }, - "wot": { - "title": "Rede de confianza", - "open": "Rede de confianza", - "help": "Cómo se decide quién ye miembru de confianza: la xente aválase ente sí, y la confianza estiéndese dende un conxuntu de raíces fundadores.", - "roots": "Raíces de confianza", - "rootsHelp": "Les identidaes fundadores qu'anclen la rede. Amiesta a quien te fíes (apega'l so códigu d'identidá); la pertenencia mídese p'afuera dende elles.", - "noRoots": "Entá nun hai raíces de confianza — nun se pue calcular la pertenencia hasta semar la rede.", - "addRoot": "Amestar una raíz de confianza", - "rootHint": "Apega un códigu d'identidá (npub…)", - "add": "Amestar", - "rootAdded": "Raíz de confianza amestada", - "rootInvalid": "Esi nun ye un códigu d'identidá válidu", - "remove": "Quitar", - "params": "Parámetros avanzaos", - "paramsHelp": "Regles Duniter. Aflóxales pa una rede nueva; apriétales según crez.", - "sigQty": "Avales pa ser miembru", - "stepMax": "Distancia máxima a una raíz", - "validityDays": "Validez del aval (díes)", - "reset": "Reafitar a valores Duniter", - "saved": "Guardáu" + "yourPeople": { + "title": "La to xente", + "help": "Persones que conoces y avales, y persones que t'avalen.", + "youVouchFor": "Tu avales a", + "vouchesForYou": "Aválente", + "youVouchForEmpty": "Entá nun avales a naide. Cuando conozas a daquién, abri la vuestra conversación y calca \"Conozo a esta persona\".", + "vouchesForYouEmpty": "Naide t'avala entá", + "revoke": "Dexar d'avalar", + "revokeConfirm": "¿Dexar d'avalar a esta persona?", + "offline": "Ensin conexón — vuelvi tentalo cuando teas en llinia" }, "notifications": { "newMessageFrom": "Mensaxe nuevu de {name}" diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index fb01bce..14ea365 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -540,29 +540,18 @@ "count": "Vouched for by {n}", "vouch": "I know this person", "vouched": "You vouch for them", - "circle": "In your circle", - "member": "Trusted member of the network" + "circle": "In your circle" }, - "wot": { - "title": "Network of trust", - "open": "Network of trust", - "help": "How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.", - "roots": "Trust roots", - "rootsHelp": "The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.", - "noRoots": "No trust roots yet — membership can't be worked out until the network is seeded.", - "addRoot": "Add a trust root", - "rootHint": "Paste an identity code (npub…)", - "add": "Add", - "rootAdded": "Trust root added", - "rootInvalid": "That's not a valid identity code", - "remove": "Remove", - "params": "Advanced parameters", - "paramsHelp": "Duniter rules. Loosen them for a young network; tighten as it grows.", - "sigQty": "Vouches to become a member", - "stepMax": "Max distance from a root", - "validityDays": "Vouch validity (days)", - "reset": "Reset to Duniter defaults", - "saved": "Saved" + "yourPeople": { + "title": "Your people", + "help": "People you've met and vouch for, and people who vouch for you.", + "youVouchFor": "You vouch for", + "vouchesForYou": "They vouch for you", + "youVouchForEmpty": "You don't vouch for anyone yet. When you meet someone, open your chat with them and tap \"I know this person\".", + "vouchesForYouEmpty": "No one vouches for you yet", + "revoke": "Stop vouching", + "revokeConfirm": "Stop vouching for this person?", + "offline": "You're offline — try again when you're connected" }, "notifications": { "newMessageFrom": "New message from {name}" diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 31281fa..5f9a231 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -539,29 +539,18 @@ "count": "Avalada por {n}", "vouch": "Conozco a esta persona", "vouched": "Avalas a esta persona", - "circle": "En tu círculo", - "member": "Miembro de confianza de la red" + "circle": "En tu círculo" }, - "wot": { - "title": "Red de confianza", - "open": "Red de confianza", - "help": "Cómo se decide quién es miembro de confianza: la gente se avala entre sí, y la confianza se extiende desde un conjunto de raíces fundadoras.", - "roots": "Raíces de confianza", - "rootsHelp": "Las identidades fundadoras que anclan la red. Añade a quien te fíes (pega su código de identidad); la membresía se mide hacia fuera desde ellas.", - "noRoots": "Aún no hay raíces de confianza — no se puede calcular la membresía hasta sembrar la red.", - "addRoot": "Añadir una raíz de confianza", - "rootHint": "Pega un código de identidad (npub…)", - "add": "Añadir", - "rootAdded": "Raíz de confianza añadida", - "rootInvalid": "Ese no es un código de identidad válido", - "remove": "Quitar", - "params": "Parámetros avanzados", - "paramsHelp": "Reglas Duniter. Aflójalas para una red joven; apriétalas según crece.", - "sigQty": "Avales para ser miembro", - "stepMax": "Distancia máxima a una raíz", - "validityDays": "Validez del aval (días)", - "reset": "Restablecer a valores Duniter", - "saved": "Guardado" + "yourPeople": { + "title": "Tu gente", + "help": "Personas que conoces y avalas, y personas que te avalan.", + "youVouchFor": "Tú avalas a", + "vouchesForYou": "Te avalan", + "youVouchForEmpty": "Aún no avalas a nadie. Cuando conozcas a alguien, abre vuestro chat y toca \"Conozco a esta persona\".", + "vouchesForYouEmpty": "Nadie te avala todavía", + "revoke": "Dejar de avalar", + "revokeConfirm": "¿Dejar de avalar a esta persona?", + "offline": "Sin conexión — inténtalo cuando estés en línea" }, "notifications": { "newMessageFrom": "Nuevo mensaje de {name}" diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index a56e202..8134f39 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -536,29 +536,18 @@ "count": "Avalizada por {n}", "vouch": "Conheço esta pessoa", "vouched": "Avalizas esta pessoa", - "circle": "No teu círculo", - "member": "Membro de confiança da rede" + "circle": "No teu círculo" }, - "wot": { - "title": "Rede de confiança", - "open": "Rede de confiança", - "help": "Como se decide quem é membro de confiança: as pessoas avalizam-se entre si, e a confiança estende-se a partir de um conjunto de raízes fundadoras.", - "roots": "Raízes de confiança", - "rootsHelp": "As identidades fundadoras que ancoram a rede. Adiciona quem confias (cola o seu código de identidade); a filiação mede-se para fora a partir delas.", - "noRoots": "Ainda não há raízes de confiança — não se pode calcular a filiação até semear a rede.", - "addRoot": "Adicionar uma raiz de confiança", - "rootHint": "Cola um código de identidade (npub…)", - "add": "Adicionar", - "rootAdded": "Raiz de confiança adicionada", - "rootInvalid": "Esse não é um código de identidade válido", - "remove": "Remover", - "params": "Parâmetros avançados", - "paramsHelp": "Regras Duniter. Afrouxa-as para uma rede jovem; aperta-as à medida que cresce.", - "sigQty": "Avais para ser membro", - "stepMax": "Distância máxima a uma raiz", - "validityDays": "Validade do aval (dias)", - "reset": "Repor para valores Duniter", - "saved": "Guardado" + "yourPeople": { + "title": "A tua gente", + "help": "Pessoas que conheces e avalizas, e pessoas que te avalizam.", + "youVouchFor": "Tu avalizas", + "vouchesForYou": "Avalizam-te", + "youVouchForEmpty": "Ainda não avalizas ninguém. Quando conheceres alguém, abre a vossa conversa e toca em \"Conheço esta pessoa\".", + "vouchesForYouEmpty": "Ainda ninguém te avaliza", + "revoke": "Deixar de avalizar", + "revokeConfirm": "Deixar de avalizar esta pessoa?", + "offline": "Sem ligação — tenta novamente quando estiveres em linha" }, "notifications": { "newMessageFrom": "Nova mensagem de {name}" diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 9880afc..5d0192c 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 4 -/// Strings: 1860 (465 per locale) +/// Strings: 1816 (454 per locale) /// -/// Built on 2026-07-11 at 04:45 UTC +/// Built on 2026-07-11 at 10:58 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 5b7b371..ac8b2e5 100644 --- a/apps/app_seeds/lib/i18n/strings_ast.g.dart +++ b/apps/app_seeds/lib/i18n/strings_ast.g.dart @@ -76,7 +76,7 @@ class TranslationsAst extends Translations with BaseTranslations 'Conozo a esta persona'; @override String get vouched => 'Avales a esta persona'; @override String get circle => 'Nel to círculu'; - @override String get member => 'Miembru de confianza de la rede'; } -// Path: wot -class _Translations$wot$ast extends Translations$wot$en { - _Translations$wot$ast._(TranslationsAst root) : this._root = root, super.internal(root); +// Path: yourPeople +class _Translations$yourPeople$ast extends Translations$yourPeople$en { + _Translations$yourPeople$ast._(TranslationsAst root) : this._root = root, super.internal(root); final TranslationsAst _root; // ignore: unused_field // Translations - @override String get title => 'Rede de confianza'; - @override String get open => 'Rede de confianza'; - @override String get help => 'Cómo se decide quién ye miembru de confianza: la xente aválase ente sí, y la confianza estiéndese dende un conxuntu de raíces fundadores.'; - @override String get roots => 'Raíces de confianza'; - @override String get rootsHelp => 'Les identidaes fundadores qu\'anclen la rede. Amiesta a quien te fíes (apega\'l so códigu d\'identidá); la pertenencia mídese p\'afuera dende elles.'; - @override String get noRoots => 'Entá nun hai raíces de confianza — nun se pue calcular la pertenencia hasta semar la rede.'; - @override String get addRoot => 'Amestar una raíz de confianza'; - @override String get rootHint => 'Apega un códigu d\'identidá (npub…)'; - @override String get add => 'Amestar'; - @override String get rootAdded => 'Raíz de confianza amestada'; - @override String get rootInvalid => 'Esi nun ye un códigu d\'identidá válidu'; - @override String get remove => 'Quitar'; - @override String get params => 'Parámetros avanzaos'; - @override String get paramsHelp => 'Regles Duniter. Aflóxales pa una rede nueva; apriétales según crez.'; - @override String get sigQty => 'Avales pa ser miembru'; - @override String get stepMax => 'Distancia máxima a una raíz'; - @override String get validityDays => 'Validez del aval (díes)'; - @override String get reset => 'Reafitar a valores Duniter'; - @override String get saved => 'Guardáu'; + @override String get title => 'La to xente'; + @override String get help => 'Persones que conoces y avales, y persones que t\'avalen.'; + @override String get youVouchFor => 'Tu avales a'; + @override String get vouchesForYou => 'Aválente'; + @override String get youVouchForEmpty => 'Entá nun avales a naide. Cuando conozas a daquién, abri la vuestra conversación y calca "Conozo a esta persona".'; + @override String get vouchesForYouEmpty => 'Naide t\'avala entá'; + @override String get revoke => 'Dexar d\'avalar'; + @override String get revokeConfirm => '¿Dexar d\'avalar a esta persona?'; + @override String get offline => 'Ensin conexón — vuelvi tentalo cuando teas en llinia'; } // Path: notifications @@ -1629,26 +1618,15 @@ extension on TranslationsAst { 'trust.vouch' => 'Conozo a esta persona', 'trust.vouched' => 'Avales a esta persona', 'trust.circle' => 'Nel to círculu', - 'trust.member' => 'Miembru de confianza de la rede', - 'wot.title' => 'Rede de confianza', - 'wot.open' => 'Rede de confianza', - 'wot.help' => 'Cómo se decide quién ye miembru de confianza: la xente aválase ente sí, y la confianza estiéndese dende un conxuntu de raíces fundadores.', - 'wot.roots' => 'Raíces de confianza', - 'wot.rootsHelp' => 'Les identidaes fundadores qu\'anclen la rede. Amiesta a quien te fíes (apega\'l so códigu d\'identidá); la pertenencia mídese p\'afuera dende elles.', - 'wot.noRoots' => 'Entá nun hai raíces de confianza — nun se pue calcular la pertenencia hasta semar la rede.', - 'wot.addRoot' => 'Amestar una raíz de confianza', - 'wot.rootHint' => 'Apega un códigu d\'identidá (npub…)', - 'wot.add' => 'Amestar', - 'wot.rootAdded' => 'Raíz de confianza amestada', - 'wot.rootInvalid' => 'Esi nun ye un códigu d\'identidá válidu', - 'wot.remove' => 'Quitar', - 'wot.params' => 'Parámetros avanzaos', - 'wot.paramsHelp' => 'Regles Duniter. Aflóxales pa una rede nueva; apriétales según crez.', - 'wot.sigQty' => 'Avales pa ser miembru', - 'wot.stepMax' => 'Distancia máxima a una raíz', - 'wot.validityDays' => 'Validez del aval (díes)', - 'wot.reset' => 'Reafitar a valores Duniter', - 'wot.saved' => 'Guardáu', + 'yourPeople.title' => 'La to xente', + 'yourPeople.help' => 'Persones que conoces y avales, y persones que t\'avalen.', + 'yourPeople.youVouchFor' => 'Tu avales a', + 'yourPeople.vouchesForYou' => 'Aválente', + 'yourPeople.youVouchForEmpty' => 'Entá nun avales a naide. Cuando conozas a daquién, abri la vuestra conversación y calca "Conozo a esta persona".', + 'yourPeople.vouchesForYouEmpty' => 'Naide t\'avala entá', + 'yourPeople.revoke' => 'Dexar d\'avalar', + 'yourPeople.revokeConfirm' => '¿Dexar d\'avalar a esta persona?', + 'yourPeople.offline' => 'Ensin conexón — vuelvi tentalo cuando teas en llinia', 'notifications.newMessageFrom' => ({required Object name}) => 'Mensaxe nuevu de ${name}', 'plantare.title' => 'Plantares', 'plantare.help' => 'Un Plantare ye\'l compromisu de reproducir una semilla y devolver una parte — asina una variedá sigue viaxando de mano en mano. Nun ye una venta.', diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index 613e78a..e3c6bff 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -77,7 +77,7 @@ class Translations with BaseTranslations { late final Translations$chatList$en chatList = Translations$chatList$en.internal(_root); late final Translations$chat$en chat = Translations$chat$en.internal(_root); late final Translations$trust$en trust = Translations$trust$en.internal(_root); - late final Translations$wot$en wot = Translations$wot$en.internal(_root); + late final Translations$yourPeople$en yourPeople = Translations$yourPeople$en.internal(_root); late final Translations$notifications$en notifications = Translations$notifications$en.internal(_root); late final Translations$plantare$en plantare = Translations$plantare$en.internal(_root); late final Translations$sale$en sale = Translations$sale$en.internal(_root); @@ -1467,75 +1467,42 @@ class Translations$trust$en { /// en: 'In your circle' String get circle => 'In your circle'; - - /// en: 'Trusted member of the network' - String get member => 'Trusted member of the network'; } -// Path: wot -class Translations$wot$en { - Translations$wot$en.internal(this._root); +// Path: yourPeople +class Translations$yourPeople$en { + Translations$yourPeople$en.internal(this._root); final Translations _root; // ignore: unused_field // Translations - /// en: 'Network of trust' - String get title => 'Network of trust'; + /// en: 'Your people' + String get title => 'Your people'; - /// en: 'Network of trust' - String get open => 'Network of trust'; + /// en: 'People you've met and vouch for, and people who vouch for you.' + String get help => 'People you\'ve met and vouch for, and people who vouch for you.'; - /// en: 'How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.' - String get help => 'How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.'; + /// en: 'You vouch for' + String get youVouchFor => 'You vouch for'; - /// en: 'Trust roots' - String get roots => 'Trust roots'; + /// en: 'They vouch for you' + String get vouchesForYou => 'They vouch for you'; - /// en: 'The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.' - String get rootsHelp => 'The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.'; + /// en: 'You don't vouch for anyone yet. When you meet someone, open your chat with them and tap "I know this person".' + String get youVouchForEmpty => 'You don\'t vouch for anyone yet. When you meet someone, open your chat with them and tap "I know this person".'; - /// en: 'No trust roots yet — membership can't be worked out until the network is seeded.' - String get noRoots => 'No trust roots yet — membership can\'t be worked out until the network is seeded.'; + /// en: 'No one vouches for you yet' + String get vouchesForYouEmpty => 'No one vouches for you yet'; - /// en: 'Add a trust root' - String get addRoot => 'Add a trust root'; + /// en: 'Stop vouching' + String get revoke => 'Stop vouching'; - /// en: 'Paste an identity code (npub…)' - String get rootHint => 'Paste an identity code (npub…)'; + /// en: 'Stop vouching for this person?' + String get revokeConfirm => 'Stop vouching for this person?'; - /// en: 'Add' - String get add => 'Add'; - - /// en: 'Trust root added' - String get rootAdded => 'Trust root added'; - - /// en: 'That's not a valid identity code' - String get rootInvalid => 'That\'s not a valid identity code'; - - /// en: 'Remove' - String get remove => 'Remove'; - - /// en: 'Advanced parameters' - String get params => 'Advanced parameters'; - - /// en: 'Duniter rules. Loosen them for a young network; tighten as it grows.' - String get paramsHelp => 'Duniter rules. Loosen them for a young network; tighten as it grows.'; - - /// en: 'Vouches to become a member' - String get sigQty => 'Vouches to become a member'; - - /// en: 'Max distance from a root' - String get stepMax => 'Max distance from a root'; - - /// en: 'Vouch validity (days)' - String get validityDays => 'Vouch validity (days)'; - - /// en: 'Reset to Duniter defaults' - String get reset => 'Reset to Duniter defaults'; - - /// en: 'Saved' - String get saved => 'Saved'; + /// en: 'You're offline — try again when you're connected' + String get offline => 'You\'re offline — try again when you\'re connected'; } // Path: notifications @@ -2549,26 +2516,15 @@ extension on Translations { 'trust.vouch' => 'I know this person', 'trust.vouched' => 'You vouch for them', 'trust.circle' => 'In your circle', - 'trust.member' => 'Trusted member of the network', - 'wot.title' => 'Network of trust', - 'wot.open' => 'Network of trust', - 'wot.help' => 'How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.', - 'wot.roots' => 'Trust roots', - 'wot.rootsHelp' => 'The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.', - 'wot.noRoots' => 'No trust roots yet — membership can\'t be worked out until the network is seeded.', - 'wot.addRoot' => 'Add a trust root', - 'wot.rootHint' => 'Paste an identity code (npub…)', - 'wot.add' => 'Add', - 'wot.rootAdded' => 'Trust root added', - 'wot.rootInvalid' => 'That\'s not a valid identity code', - 'wot.remove' => 'Remove', - 'wot.params' => 'Advanced parameters', - 'wot.paramsHelp' => 'Duniter rules. Loosen them for a young network; tighten as it grows.', - 'wot.sigQty' => 'Vouches to become a member', - 'wot.stepMax' => 'Max distance from a root', - 'wot.validityDays' => 'Vouch validity (days)', - 'wot.reset' => 'Reset to Duniter defaults', - 'wot.saved' => 'Saved', + 'yourPeople.title' => 'Your people', + 'yourPeople.help' => 'People you\'ve met and vouch for, and people who vouch for you.', + 'yourPeople.youVouchFor' => 'You vouch for', + 'yourPeople.vouchesForYou' => 'They vouch for you', + 'yourPeople.youVouchForEmpty' => 'You don\'t vouch for anyone yet. When you meet someone, open your chat with them and tap "I know this person".', + 'yourPeople.vouchesForYouEmpty' => 'No one vouches for you yet', + 'yourPeople.revoke' => 'Stop vouching', + 'yourPeople.revokeConfirm' => 'Stop vouching for this person?', + 'yourPeople.offline' => 'You\'re offline — try again when you\'re connected', 'notifications.newMessageFrom' => ({required Object name}) => 'New message from ${name}', 'plantare.title' => 'Plantares', 'plantare.help' => 'A Plantare is a promise to reproduce a seed and return some — how a variety keeps travelling from hand to hand. It\'s not a sale.', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index a157b53..67c1fab 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -76,7 +76,7 @@ class TranslationsEs extends Translations with BaseTranslations 'Conozco a esta persona'; @override String get vouched => 'Avalas a esta persona'; @override String get circle => 'En tu círculo'; - @override String get member => 'Miembro de confianza de la red'; } -// Path: wot -class _Translations$wot$es extends Translations$wot$en { - _Translations$wot$es._(TranslationsEs root) : this._root = root, super.internal(root); +// Path: yourPeople +class _Translations$yourPeople$es extends Translations$yourPeople$en { + _Translations$yourPeople$es._(TranslationsEs root) : this._root = root, super.internal(root); final TranslationsEs _root; // ignore: unused_field // Translations - @override String get title => 'Red de confianza'; - @override String get open => 'Red de confianza'; - @override String get help => 'Cómo se decide quién es miembro de confianza: la gente se avala entre sí, y la confianza se extiende desde un conjunto de raíces fundadoras.'; - @override String get roots => 'Raíces de confianza'; - @override String get rootsHelp => 'Las identidades fundadoras que anclan la red. Añade a quien te fíes (pega su código de identidad); la membresía se mide hacia fuera desde ellas.'; - @override String get noRoots => 'Aún no hay raíces de confianza — no se puede calcular la membresía hasta sembrar la red.'; - @override String get addRoot => 'Añadir una raíz de confianza'; - @override String get rootHint => 'Pega un código de identidad (npub…)'; - @override String get add => 'Añadir'; - @override String get rootAdded => 'Raíz de confianza añadida'; - @override String get rootInvalid => 'Ese no es un código de identidad válido'; - @override String get remove => 'Quitar'; - @override String get params => 'Parámetros avanzados'; - @override String get paramsHelp => 'Reglas Duniter. Aflójalas para una red joven; apriétalas según crece.'; - @override String get sigQty => 'Avales para ser miembro'; - @override String get stepMax => 'Distancia máxima a una raíz'; - @override String get validityDays => 'Validez del aval (días)'; - @override String get reset => 'Restablecer a valores Duniter'; - @override String get saved => 'Guardado'; + @override String get title => 'Tu gente'; + @override String get help => 'Personas que conoces y avalas, y personas que te avalan.'; + @override String get youVouchFor => 'Tú avalas a'; + @override String get vouchesForYou => 'Te avalan'; + @override String get youVouchForEmpty => 'Aún no avalas a nadie. Cuando conozcas a alguien, abre vuestro chat y toca "Conozco a esta persona".'; + @override String get vouchesForYouEmpty => 'Nadie te avala todavía'; + @override String get revoke => 'Dejar de avalar'; + @override String get revokeConfirm => '¿Dejar de avalar a esta persona?'; + @override String get offline => 'Sin conexión — inténtalo cuando estés en línea'; } // Path: notifications @@ -1633,26 +1622,15 @@ extension on TranslationsEs { 'trust.vouch' => 'Conozco a esta persona', 'trust.vouched' => 'Avalas a esta persona', 'trust.circle' => 'En tu círculo', - 'trust.member' => 'Miembro de confianza de la red', - 'wot.title' => 'Red de confianza', - 'wot.open' => 'Red de confianza', - 'wot.help' => 'Cómo se decide quién es miembro de confianza: la gente se avala entre sí, y la confianza se extiende desde un conjunto de raíces fundadoras.', - 'wot.roots' => 'Raíces de confianza', - 'wot.rootsHelp' => 'Las identidades fundadoras que anclan la red. Añade a quien te fíes (pega su código de identidad); la membresía se mide hacia fuera desde ellas.', - 'wot.noRoots' => 'Aún no hay raíces de confianza — no se puede calcular la membresía hasta sembrar la red.', - 'wot.addRoot' => 'Añadir una raíz de confianza', - 'wot.rootHint' => 'Pega un código de identidad (npub…)', - 'wot.add' => 'Añadir', - 'wot.rootAdded' => 'Raíz de confianza añadida', - 'wot.rootInvalid' => 'Ese no es un código de identidad válido', - 'wot.remove' => 'Quitar', - 'wot.params' => 'Parámetros avanzados', - 'wot.paramsHelp' => 'Reglas Duniter. Aflójalas para una red joven; apriétalas según crece.', - 'wot.sigQty' => 'Avales para ser miembro', - 'wot.stepMax' => 'Distancia máxima a una raíz', - 'wot.validityDays' => 'Validez del aval (días)', - 'wot.reset' => 'Restablecer a valores Duniter', - 'wot.saved' => 'Guardado', + 'yourPeople.title' => 'Tu gente', + 'yourPeople.help' => 'Personas que conoces y avalas, y personas que te avalan.', + 'yourPeople.youVouchFor' => 'Tú avalas a', + 'yourPeople.vouchesForYou' => 'Te avalan', + 'yourPeople.youVouchForEmpty' => 'Aún no avalas a nadie. Cuando conozcas a alguien, abre vuestro chat y toca "Conozco a esta persona".', + 'yourPeople.vouchesForYouEmpty' => 'Nadie te avala todavía', + 'yourPeople.revoke' => 'Dejar de avalar', + 'yourPeople.revokeConfirm' => '¿Dejar de avalar a esta persona?', + 'yourPeople.offline' => 'Sin conexión — inténtalo cuando estés en línea', 'notifications.newMessageFrom' => ({required Object name}) => 'Nuevo mensaje de ${name}', 'plantare.title' => 'Plantares', 'plantare.help' => 'Un Plantare es el compromiso de reproducir una semilla y devolver una parte — así una variedad sigue viajando de mano en mano. No es una venta.', diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index 4590a46..26f46c8 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -76,7 +76,7 @@ class TranslationsPt extends Translations with BaseTranslations 'Conheço esta pessoa'; @override String get vouched => 'Avalizas esta pessoa'; @override String get circle => 'No teu círculo'; - @override String get member => 'Membro de confiança da rede'; } -// Path: wot -class _Translations$wot$pt extends Translations$wot$en { - _Translations$wot$pt._(TranslationsPt root) : this._root = root, super.internal(root); +// Path: yourPeople +class _Translations$yourPeople$pt extends Translations$yourPeople$en { + _Translations$yourPeople$pt._(TranslationsPt root) : this._root = root, super.internal(root); final TranslationsPt _root; // ignore: unused_field // Translations - @override String get title => 'Rede de confiança'; - @override String get open => 'Rede de confiança'; - @override String get help => 'Como se decide quem é membro de confiança: as pessoas avalizam-se entre si, e a confiança estende-se a partir de um conjunto de raízes fundadoras.'; - @override String get roots => 'Raízes de confiança'; - @override String get rootsHelp => 'As identidades fundadoras que ancoram a rede. Adiciona quem confias (cola o seu código de identidade); a filiação mede-se para fora a partir delas.'; - @override String get noRoots => 'Ainda não há raízes de confiança — não se pode calcular a filiação até semear a rede.'; - @override String get addRoot => 'Adicionar uma raiz de confiança'; - @override String get rootHint => 'Cola um código de identidade (npub…)'; - @override String get add => 'Adicionar'; - @override String get rootAdded => 'Raiz de confiança adicionada'; - @override String get rootInvalid => 'Esse não é um código de identidade válido'; - @override String get remove => 'Remover'; - @override String get params => 'Parâmetros avançados'; - @override String get paramsHelp => 'Regras Duniter. Afrouxa-as para uma rede jovem; aperta-as à medida que cresce.'; - @override String get sigQty => 'Avais para ser membro'; - @override String get stepMax => 'Distância máxima a uma raiz'; - @override String get validityDays => 'Validade do aval (dias)'; - @override String get reset => 'Repor para valores Duniter'; - @override String get saved => 'Guardado'; + @override String get title => 'A tua gente'; + @override String get help => 'Pessoas que conheces e avalizas, e pessoas que te avalizam.'; + @override String get youVouchFor => 'Tu avalizas'; + @override String get vouchesForYou => 'Avalizam-te'; + @override String get youVouchForEmpty => 'Ainda não avalizas ninguém. Quando conheceres alguém, abre a vossa conversa e toca em "Conheço esta pessoa".'; + @override String get vouchesForYouEmpty => 'Ainda ninguém te avaliza'; + @override String get revoke => 'Deixar de avalizar'; + @override String get revokeConfirm => 'Deixar de avalizar esta pessoa?'; + @override String get offline => 'Sem ligação — tenta novamente quando estiveres em linha'; } // Path: notifications @@ -1627,26 +1616,15 @@ extension on TranslationsPt { 'trust.vouch' => 'Conheço esta pessoa', 'trust.vouched' => 'Avalizas esta pessoa', 'trust.circle' => 'No teu círculo', - 'trust.member' => 'Membro de confiança da rede', - 'wot.title' => 'Rede de confiança', - 'wot.open' => 'Rede de confiança', - 'wot.help' => 'Como se decide quem é membro de confiança: as pessoas avalizam-se entre si, e a confiança estende-se a partir de um conjunto de raízes fundadoras.', - 'wot.roots' => 'Raízes de confiança', - 'wot.rootsHelp' => 'As identidades fundadoras que ancoram a rede. Adiciona quem confias (cola o seu código de identidade); a filiação mede-se para fora a partir delas.', - 'wot.noRoots' => 'Ainda não há raízes de confiança — não se pode calcular a filiação até semear a rede.', - 'wot.addRoot' => 'Adicionar uma raiz de confiança', - 'wot.rootHint' => 'Cola um código de identidade (npub…)', - 'wot.add' => 'Adicionar', - 'wot.rootAdded' => 'Raiz de confiança adicionada', - 'wot.rootInvalid' => 'Esse não é um código de identidade válido', - 'wot.remove' => 'Remover', - 'wot.params' => 'Parâmetros avançados', - 'wot.paramsHelp' => 'Regras Duniter. Afrouxa-as para uma rede jovem; aperta-as à medida que cresce.', - 'wot.sigQty' => 'Avais para ser membro', - 'wot.stepMax' => 'Distância máxima a uma raiz', - 'wot.validityDays' => 'Validade do aval (dias)', - 'wot.reset' => 'Repor para valores Duniter', - 'wot.saved' => 'Guardado', + 'yourPeople.title' => 'A tua gente', + 'yourPeople.help' => 'Pessoas que conheces e avalizas, e pessoas que te avalizam.', + 'yourPeople.youVouchFor' => 'Tu avalizas', + 'yourPeople.vouchesForYou' => 'Avalizam-te', + 'yourPeople.youVouchForEmpty' => 'Ainda não avalizas ninguém. Quando conheceres alguém, abre a vossa conversa e toca em "Conheço esta pessoa".', + 'yourPeople.vouchesForYouEmpty' => 'Ainda ninguém te avaliza', + 'yourPeople.revoke' => 'Deixar de avalizar', + 'yourPeople.revokeConfirm' => 'Deixar de avalizar esta pessoa?', + 'yourPeople.offline' => 'Sem ligação — tenta novamente quando estiveres em linha', 'notifications.newMessageFrom' => ({required Object name}) => 'Nova mensagem de ${name}', 'plantare.title' => 'Plantares', 'plantare.help' => 'Um Plantare é o compromisso de reproduzir uma semente e devolver uma parte — assim uma variedade continua a viajar de mão em mão. Não é uma venda.', diff --git a/apps/app_seeds/lib/services/trust_referents.dart b/apps/app_seeds/lib/services/trust_referents.dart deleted file mode 100644 index 3762e93..0000000 --- a/apps/app_seeds/lib/services/trust_referents.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:convert'; - -import 'package:commons_core/commons_core.dart'; -import 'package:flutter/services.dart' show rootBundle; - -import '../security/secret_store.dart'; - -/// The web-of-trust bootstrap referents (Duniter "seeds"): identities every -/// membership calculation trusts by construction. The network's roots. -/// -/// Sources, unioned: a bundled asset (`assets/trust/referents.json`, a curated -/// founding set — empty until real founders are added) plus referents the user -/// adds in-app (by npub / QR), kept in the keystore. All keys are stored and -/// returned as 64-char hex. Solving cold-start honestly: no invented identities -/// ship, and a user can bootstrap their own roots. -class TrustReferents { - TrustReferents(this._store, {this.assetPath = 'assets/trust/referents.json'}); - - final SecretStore _store; - final String assetPath; - - static const _userKey = 'tane.social.trust.referents'; - - List? _bundledCache; - - /// Every referent (bundled ∪ user-added), as hex public keys. - Future> all() async => - {...await _bundled(), ...await userAdded()}; - - /// Referents the user added themselves (hex), for the management UI. - Future> userAdded() async { - final raw = await _store.read(_userKey); - if (raw == null || raw.isEmpty) return {}; - return raw.split('\n').where((s) => s.isNotEmpty).toSet(); - } - - /// Adds a referent from an [npubOrHex]; returns its hex form. Idempotent. - /// Throws [FormatException] if it is neither an npub nor a hex key. - Future add(String npubOrHex) async { - final hex = npubToHex(npubOrHex); // validates - final current = await userAdded(); - if (current.add(hex)) { - await _store.write(_userKey, current.join('\n')); - } - return hex; - } - - /// Removes a user-added referent (bundled ones can't be removed). - Future remove(String hex) async { - final current = await userAdded()..remove(hex); - await _store.write(_userKey, current.join('\n')); - } - - Future> _bundled() async { - final cached = _bundledCache; - if (cached != null) return cached; - try { - final decoded = jsonDecode(await rootBundle.loadString(assetPath)); - final list = (decoded is Map ? decoded['referents'] : decoded) as List?; - final out = []; - for (final entry in list ?? const []) { - try { - out.add(npubToHex(entry as String)); - } catch (_) { - // Skip a malformed entry rather than break the whole set. - } - } - return _bundledCache = out; - } catch (_) { - return _bundledCache = const []; // no/invalid asset → no bundled referents - } - } -} diff --git a/apps/app_seeds/lib/services/wot_settings.dart b/apps/app_seeds/lib/services/wot_settings.dart deleted file mode 100644 index d2833b5..0000000 --- a/apps/app_seeds/lib/services/wot_settings.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:commons_core/commons_core.dart'; - -import '../security/secret_store.dart'; - -/// Persists the web-of-trust parameters (Duniter sigQty / stepMax / validity), -/// so a young network can loosen them and tighten as it grows — exactly how Ğ1 -/// itself evolved. Defaults to [WotParams.duniter]. Keystore-backed. -class WotSettings { - WotSettings(this._store); - - final SecretStore _store; - - static const _sigQtyKey = 'tane.social.wot.sigQty'; - static const _stepMaxKey = 'tane.social.wot.stepMax'; - static const _validityDaysKey = 'tane.social.wot.validityDays'; - - /// The active parameters (any unset field falls back to the Duniter default). - Future params() async { - const base = WotParams.duniter; - final sigQty = await _readInt(_sigQtyKey) ?? base.sigQty; - final stepMax = await _readInt(_stepMaxKey) ?? base.stepMax; - final validityDays = - await _readInt(_validityDaysKey) ?? base.sigValidity.inDays; - return WotParams( - sigQty: sigQty, - stepMax: stepMax, - sigValidity: Duration(days: validityDays), - ); - } - - Future save(WotParams params) async { - await _store.write(_sigQtyKey, '${params.sigQty}'); - await _store.write(_stepMaxKey, '${params.stepMax}'); - await _store.write(_validityDaysKey, '${params.sigValidity.inDays}'); - } - - /// Restores the Ğ1 reference values. - Future reset() => save(WotParams.duniter); - - Future _readInt(String key) async { - final raw = await _store.read(key); - if (raw == null || raw.isEmpty) return null; - return int.tryParse(raw); - } -} diff --git a/apps/app_seeds/lib/state/trust_cubit.dart b/apps/app_seeds/lib/state/trust_cubit.dart index 2347302..21fd4f4 100644 --- a/apps/app_seeds/lib/state/trust_cubit.dart +++ b/apps/app_seeds/lib/state/trust_cubit.dart @@ -2,32 +2,28 @@ import 'package:commons_core/commons_core.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -/// Where a peer stands, from strongest to weakest signal. +/// Where a peer stands, from strongest to weakest signal. Trust is +/// ego-centric: computed from YOUR position in the public vouch graph, +/// never from a global membership verdict. enum TrustTier { - /// Passes the full Duniter membership rule (sigQty certifications from members, - /// within stepMax of a bootstrap referent). - networkMember, - /// In your personal circle (you vouch, or a friend-of-a-friend does). inYourCircle, - /// Vouched for by someone, but not (yet) a member nor in your circle. + /// Vouched for by someone, but not (yet) in your circle. vouched, /// No certifications seen. unknown, } -/// Trust standing of one peer: the full Duniter membership verdict, your own -/// circle, and the raw certifier count — computed from the public certification -/// graph with the active [WotParams] and bootstrap referents. +/// Trust standing of one peer, seen from this user's position: your own +/// vouch, your circle (friend-of-a-friend), and the raw certifier count — +/// computed from the public certification graph. class TrustState extends Equatable { const TrustState({ this.certifierCount = 0, this.iVouch = false, this.knownToYou = false, - this.isNetworkMember = false, - this.networkBootstrapped = false, this.loading = true, this.busy = false, }); @@ -42,21 +38,11 @@ class TrustState extends Equatable { /// friend-of-a-friend, vouch). Spam-resistant and works from day one. final bool knownToYou; - /// Whether the peer is a full member per the Duniter rule (sigQty + stepMax - /// from the bootstrap referents). - final bool isNetworkMember; - - /// Whether any bootstrap referents exist at all. When false, network - /// membership can't be determined yet (the trust net isn't seeded) and the UI - /// should say so instead of implying the peer failed the rule. - final bool networkBootstrapped; - final bool loading; final bool busy; /// The strongest applicable signal, for the badge. TrustTier get tier { - if (isNetworkMember) return TrustTier.networkMember; if (knownToYou) return TrustTier.inYourCircle; if (certifierCount > 0) return TrustTier.vouched; return TrustTier.unknown; @@ -66,8 +52,6 @@ class TrustState extends Equatable { int? certifierCount, bool? iVouch, bool? knownToYou, - bool? isNetworkMember, - bool? networkBootstrapped, bool? loading, bool? busy, }) => @@ -75,8 +59,6 @@ class TrustState extends Equatable { certifierCount: certifierCount ?? this.certifierCount, iVouch: iVouch ?? this.iVouch, knownToYou: knownToYou ?? this.knownToYou, - isNetworkMember: isNetworkMember ?? this.isNetworkMember, - networkBootstrapped: networkBootstrapped ?? this.networkBootstrapped, loading: loading ?? this.loading, busy: busy ?? this.busy, ); @@ -86,23 +68,19 @@ class TrustState extends Equatable { certifierCount, iVouch, knownToYou, - isNetworkMember, - networkBootstrapped, loading, busy, ]; } -/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport], -/// and computes the full Duniter membership verdict against the bootstrap -/// [referents] and active [params]. +/// Reads and toggles this user's vouch for [peerPubkey] over a +/// [TrustTransport], and computes the peer's standing from this user's own +/// position in the vouch graph (ego-centric — no referents, no parameters). class TrustCubit extends Cubit { TrustCubit( this._transport, { required this.peerPubkey, required this.selfPubkey, - this.referents = const {}, - this.params = WotParams.duniter, Future Function()? onDispose, }) : _onDispose = onDispose, super(const TrustState()); @@ -110,26 +88,20 @@ class TrustCubit extends Cubit { final TrustTransport? _transport; final String peerPubkey; final String selfPubkey; - - /// Bootstrap referents (Duniter seeds) the membership rule trusts by - /// construction. Empty = the network isn't seeded yet. - final Set referents; - - /// Active web-of-trust parameters (sigQty / stepMax / validity). - final WotParams params; final Future Function()? _onDispose; bool get isOnline => _transport != null; /// Personal "circle" rule: one vouch from your side, out to a friend-of-a- - /// friend. Loose by design — "people near you", separate from formal - /// membership. + /// friend. Loose by design — "people near you". static const _circleThreshold = 1; static const _circleDistance = 2; + /// Vouches expire and must be renewed, so stale trust prunes itself. + static const _vouchValidity = Duration(days: 365); + /// Loads the certification graph and computes: the peer's certifier count, - /// whether you vouch, whether they're in your circle, and whether they pass - /// the full Duniter membership rule against the bootstrap referents. + /// whether you vouch, and whether they're in your circle. Future load() async { final transport = _transport; if (transport == null) { @@ -147,23 +119,17 @@ class TrustCubit extends Cubit { threshold: _circleThreshold, maxDistance: _circleDistance, ); - // Full membership: only meaningful once the trust net has referents. - final members = referents.isEmpty - ? const {} - : wot.membersWith(seeds: referents, params: params); emit(state.copyWith( certifierCount: certifiers.length, iVouch: certifiers.contains(selfPubkey), knownToYou: circle.contains(peerPubkey), - isNetworkMember: members.contains(peerPubkey), - networkBootstrapped: referents.isNotEmpty, loading: false, )); } /// Adds or removes this user's vouch, then reloads. Never vouches for self. - /// The certification is issued with the active validity so it expires and - /// must be renewed (Duniter rule). + /// The certification is issued with a validity so it expires and must be + /// renewed. Future toggleVouch() async { final transport = _transport; if (transport == null || peerPubkey == selfPubkey) return; @@ -173,7 +139,7 @@ class TrustCubit extends Cubit { } else { await transport.certify( subjectPubkey: peerPubkey, - validity: params.sigValidity, + validity: _vouchValidity, ); } await load(); diff --git a/apps/app_seeds/lib/ui/chat_screen.dart b/apps/app_seeds/lib/ui/chat_screen.dart index 1bdcac8..24e0087 100644 --- a/apps/app_seeds/lib/ui/chat_screen.dart +++ b/apps/app_seeds/lib/ui/chat_screen.dart @@ -13,9 +13,7 @@ import '../services/message_store.dart'; import '../services/profile_cache.dart'; import '../services/social_connection.dart'; import '../services/social_service.dart'; -import '../services/trust_referents.dart'; import '../services/unread_service.dart'; -import '../services/wot_settings.dart'; import '../state/messages_cubit.dart'; import '../state/trust_cubit.dart'; import 'peer_avatar.dart'; @@ -31,8 +29,6 @@ class ChatScreen extends StatefulWidget { required this.peerPubkey, this.messageStore, this.profileCache, - this.trustReferents, - this.wotSettings, super.key, }); @@ -49,10 +45,6 @@ class ChatScreen extends StatefulWidget { /// Optional cache of peer display names; null in tests. final ProfileCache? profileCache; - /// Web-of-trust bootstrap referents + parameters, for the membership verdict. - final TrustReferents? trustReferents; - final WotSettings? wotSettings; - @override State createState() => _ChatScreenState(); } @@ -88,8 +80,6 @@ class _ChatScreenState extends State { // connected / unreachable) → null transports and the screen degrades. final session = await widget.connection.session(); final cachedName = await widget.profileCache?.name(widget.peerPubkey); - final referents = await widget.trustReferents?.all() ?? const {}; - final wotParams = await widget.wotSettings?.params() ?? WotParams.duniter; if (!mounted) return; // shared session is owned by the connection, not us final self = widget.social.publicKeyHex; final messages = MessagesCubit( @@ -102,8 +92,6 @@ class _ChatScreenState extends State { session?.trust, peerPubkey: widget.peerPubkey, selfPubkey: self, - referents: referents, - params: wotParams, ); unawaited(trust.load()); setState(() { @@ -323,10 +311,9 @@ class _TrustBanner extends StatelessWidget { builder: (context, state) { final cubit = context.read(); if (!cubit.isOnline || state.loading) return const SizedBox.shrink(); - // Strongest applicable signal decides the badge (member > circle > - // vouched > unknown). + // Strongest applicable signal decides the badge (circle > vouched > + // unknown). final (IconData icon, String label, bool strong) = switch (state.tier) { - TrustTier.networkMember => (Icons.verified, t.trust.member, true), TrustTier.inYourCircle => (Icons.verified_user, t.trust.circle, true), TrustTier.vouched => ( Icons.people_outline, diff --git a/apps/app_seeds/lib/ui/profile_screen.dart b/apps/app_seeds/lib/ui/profile_screen.dart index 1eb940c..54b429c 100644 --- a/apps/app_seeds/lib/ui/profile_screen.dart +++ b/apps/app_seeds/lib/ui/profile_screen.dart @@ -22,7 +22,7 @@ class ProfileScreen extends StatefulWidget { required this.connection, required this.profileStore, required this.accounts, - this.trustNetworkEnabled = false, + this.yourPeopleEnabled = false, super.key, }); @@ -33,8 +33,8 @@ class ProfileScreen extends StatefulWidget { final ProfileStore profileStore; final SocialAccountStore accounts; - /// Whether to offer the "Network of trust" entry (needs the WoT services). - final bool trustNetworkEnabled; + /// Whether to offer the "Your people" entry (needs the social layer). + final bool yourPeopleEnabled; @override State createState() => _ProfileScreenState(); @@ -210,16 +210,17 @@ class _ProfileScreenState extends State { onSwitch: (a) => _switchTo(a), onNew: _newIdentity, ), - if (widget.trustNetworkEnabled) ...[ + if (widget.yourPeopleEnabled) ...[ const SizedBox(height: 12), const Divider(), ListTile( - key: const Key('profile.trustNetwork'), + key: const Key('profile.yourPeople'), contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.hub_outlined, color: seedGreen), - title: Text(t.wot.open), + leading: + const Icon(Icons.people_alt_outlined, color: seedGreen), + title: Text(t.yourPeople.title), trailing: const Icon(Icons.chevron_right, color: seedMuted), - onTap: () => context.push('/trust'), + onTap: () => context.push('/your-people'), ), ], ], diff --git a/apps/app_seeds/lib/ui/trust_network_screen.dart b/apps/app_seeds/lib/ui/trust_network_screen.dart deleted file mode 100644 index a0d5e8b..0000000 --- a/apps/app_seeds/lib/ui/trust_network_screen.dart +++ /dev/null @@ -1,228 +0,0 @@ -import 'package:commons_core/commons_core.dart'; -import 'package:flutter/material.dart'; - -import '../i18n/strings.g.dart'; -import '../services/profile_cache.dart' show shortPubkey; -import '../services/trust_referents.dart'; -import '../services/wot_settings.dart'; -import 'theme.dart'; - -/// Manage the web of trust: the bootstrap "roots" (Duniter seeds) membership is -/// measured from, and the advanced parameters (sigQty / stepMax / validity). -/// This is where a young network is seeded and tuned — progressive disclosure, -/// reached from the profile. -class TrustNetworkScreen extends StatefulWidget { - const TrustNetworkScreen({ - required this.referents, - required this.wotSettings, - super.key, - }); - - final TrustReferents referents; - final WotSettings wotSettings; - - @override - State createState() => _TrustNetworkScreenState(); -} - -class _TrustNetworkScreenState extends State { - final _input = TextEditingController(); - Set _roots = {}; - WotParams _params = WotParams.duniter; - bool _loading = true; - String? _error; - - @override - void initState() { - super.initState(); - _load(); - } - - Future _load() async { - final roots = await widget.referents.userAdded(); - final params = await widget.wotSettings.params(); - if (!mounted) return; - setState(() { - _roots = roots; - _params = params; - _loading = false; - }); - } - - Future _addRoot() async { - final t = context.t; - final messenger = ScaffoldMessenger.of(context); - try { - await widget.referents.add(_input.text); - _input.clear(); - await _load(); - messenger.showSnackBar(SnackBar(content: Text(t.wot.rootAdded))); - } on FormatException { - setState(() => _error = t.wot.rootInvalid); - } - } - - Future _removeRoot(String hex) async { - await widget.referents.remove(hex); - await _load(); - } - - Future _saveParams(WotParams next) async { - await widget.wotSettings.save(next); - if (!mounted) return; - setState(() => _params = next); - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(context.t.wot.saved))); - } - - @override - void dispose() { - _input.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final t = context.t; - return Scaffold( - appBar: AppBar(title: Text(t.wot.title)), - body: _loading - ? const Center(child: CircularProgressIndicator()) - : ListView( - padding: const EdgeInsets.all(20), - children: [ - Text(t.wot.help, - style: const TextStyle(color: seedMuted, fontSize: 13)), - const SizedBox(height: 20), - Text(t.wot.roots, - style: const TextStyle( - fontWeight: FontWeight.w600, color: seedOnSurface)), - const SizedBox(height: 4), - Text(t.wot.rootsHelp, - style: const TextStyle(color: seedMuted, fontSize: 12)), - const SizedBox(height: 8), - if (_roots.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text(t.wot.noRoots, - style: - const TextStyle(color: seedMuted, fontSize: 13)), - ), - for (final hex in _roots) - ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.hub_outlined, color: seedGreen), - title: Text(shortPubkey(hex), - style: const TextStyle( - fontFamily: 'monospace', fontSize: 13)), - trailing: IconButton( - icon: const Icon(Icons.close, size: 20), - tooltip: t.wot.remove, - onPressed: () => _removeRoot(hex), - ), - ), - const SizedBox(height: 8), - TextField( - key: const Key('wot.rootInput'), - controller: _input, - onChanged: (_) { - if (_error != null) setState(() => _error = null); - }, - decoration: InputDecoration( - labelText: t.wot.addRoot, - hintText: t.wot.rootHint, - errorText: _error, - suffixIcon: IconButton( - key: const Key('wot.addRoot'), - icon: const Icon(Icons.add), - onPressed: _addRoot, - ), - ), - ), - const SizedBox(height: 12), - const Divider(), - ExpansionTile( - tilePadding: EdgeInsets.zero, - title: Text(t.wot.params), - subtitle: Text(t.wot.paramsHelp, - style: const TextStyle(fontSize: 12)), - children: [ - _Stepper( - label: t.wot.sigQty, - value: _params.sigQty, - min: 1, - onChanged: (v) => - _saveParams(_params.copyWith(sigQty: v)), - ), - _Stepper( - label: t.wot.stepMax, - value: _params.stepMax, - min: 1, - onChanged: (v) => - _saveParams(_params.copyWith(stepMax: v)), - ), - _Stepper( - label: t.wot.validityDays, - value: _params.sigValidity.inDays, - min: 1, - step: 30, - onChanged: (v) => _saveParams( - _params.copyWith(sigValidity: Duration(days: v))), - ), - const SizedBox(height: 8), - Align( - alignment: AlignmentDirectional.centerStart, - child: TextButton( - onPressed: () => _saveParams(WotParams.duniter), - child: Text(t.wot.reset), - ), - ), - ], - ), - ], - ), - ); - } -} - -/// A "- value +" row for an integer parameter, clamped to [min]. -class _Stepper extends StatelessWidget { - const _Stepper({ - required this.label, - required this.value, - required this.onChanged, - this.min = 0, - this.step = 1, - }); - - final String label; - final int value; - final int min; - final int step; - final ValueChanged onChanged; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - Expanded(child: Text(label)), - IconButton( - icon: const Icon(Icons.remove_circle_outline), - onPressed: - value - step >= min ? () => onChanged(value - step) : null, - ), - SizedBox( - width: 44, - child: Text('$value', textAlign: TextAlign.center), - ), - IconButton( - icon: const Icon(Icons.add_circle_outline), - onPressed: () => onChanged(value + step), - ), - ], - ), - ); - } -} diff --git a/apps/app_seeds/lib/ui/your_people_screen.dart b/apps/app_seeds/lib/ui/your_people_screen.dart new file mode 100644 index 0000000..8ca1f0b --- /dev/null +++ b/apps/app_seeds/lib/ui/your_people_screen.dart @@ -0,0 +1,240 @@ +import 'dart:async'; + +import 'package:commons_core/commons_core.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../i18n/strings.g.dart'; +import '../services/profile_cache.dart'; +import '../services/social_connection.dart'; +import '../services/social_service.dart'; +import 'theme.dart'; + +/// "Your people": who you vouch for and who vouches for you, in human +/// language. Ego-centric by design — no roots, no parameters, no graph talk. +/// Reached from the profile; rows open the 1:1 chat. +class YourPeopleScreen extends StatefulWidget { + const YourPeopleScreen({ + required this.social, + required this.connection, + this.profileCache, + super.key, + }); + + final SocialService social; + + /// The shared relay connection (one per identity), carrying trust. + final SocialConnection connection; + + /// Optional cache of peer display names; null in tests. + final ProfileCache? profileCache; + + @override + State createState() => _YourPeopleScreenState(); +} + +class _YourPeopleScreenState extends State { + TrustTransport? _transport; + bool _loading = true; + bool _busy = false; + List _youVouchFor = const []; + List _vouchForYou = const []; + final Map _names = {}; + + @override + void initState() { + super.initState(); + _init(); + } + + Future _init() async { + final session = await widget.connection.session(); + if (!mounted) return; // shared session is owned by the connection, not us + _transport = session?.trust; + await _load(); + } + + Future _load() async { + final transport = _transport; + if (transport == null) { + if (mounted) setState(() => _loading = false); + return; + } + final self = widget.social.publicKeyHex; + final now = DateTime.now(); + final certs = (await transport.allCertifications()) + .where((c) => c.isValidAt(now)) + .toList(); + final given = [ + for (final c in certs) + if (c.issuer == self) c.subject, + ]..sort(); + final received = [ + for (final c in certs) + if (c.subject == self) c.issuer, + ]..sort(); + final cache = widget.profileCache; + if (cache != null) { + for (final peer in {...given, ...received}) { + final name = await cache.name(peer); + if (name != null) _names[peer] = name; + } + } + if (!mounted) return; + setState(() { + _youVouchFor = given; + _vouchForYou = received; + _loading = false; + }); + } + + Future _revoke(String peer) async { + final t = context.t; + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(t.yourPeople.revokeConfirm), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(t.common.cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: Text(t.yourPeople.revoke), + ), + ], + ), + ); + final transport = _transport; + if (confirmed != true || transport == null || !mounted) return; + setState(() => _busy = true); + await transport.revoke(subjectPubkey: peer); + await _load(); + if (mounted) setState(() => _busy = false); + } + + @override + Widget build(BuildContext context) { + final t = context.t; + return Scaffold( + appBar: AppBar(title: Text(t.yourPeople.title)), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : _transport == null + ? Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text( + t.yourPeople.offline, + textAlign: TextAlign.center, + style: const TextStyle(color: seedMuted, fontSize: 15), + ), + ), + ) + : ListView( + padding: const EdgeInsets.symmetric(vertical: 8), + children: [ + Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + 16, 8, 16, 8), + child: Text( + t.yourPeople.help, + style: + const TextStyle(color: seedMuted, fontSize: 14), + ), + ), + _SectionHeader(label: t.yourPeople.youVouchFor), + if (_youVouchFor.isEmpty) + _EmptyNote(text: t.yourPeople.youVouchForEmpty) + else + for (final peer in _youVouchFor) + _PersonTile( + key: Key('yourPeople.given.$peer'), + name: _names[peer] ?? shortPubkey(peer), + onOpen: () => context.push('/chat/$peer'), + trailing: TextButton( + onPressed: _busy ? null : () => _revoke(peer), + child: Text(t.yourPeople.revoke), + ), + ), + const SizedBox(height: 12), + _SectionHeader(label: t.yourPeople.vouchesForYou), + if (_vouchForYou.isEmpty) + _EmptyNote(text: t.yourPeople.vouchesForYouEmpty) + else + for (final peer in _vouchForYou) + _PersonTile( + key: Key('yourPeople.received.$peer'), + name: _names[peer] ?? shortPubkey(peer), + onOpen: () => context.push('/chat/$peer'), + ), + ], + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + const _SectionHeader({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsDirectional.fromSTEB(16, 12, 16, 4), + child: Text( + label, + style: const TextStyle( + color: seedGreen, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} + +class _EmptyNote extends StatelessWidget { + const _EmptyNote({required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsDirectional.fromSTEB(16, 4, 16, 4), + child: Text( + text, + style: const TextStyle(color: seedMuted, fontSize: 14), + ), + ); + } +} + +class _PersonTile extends StatelessWidget { + const _PersonTile({ + required this.name, + required this.onOpen, + this.trailing, + super.key, + }); + + final String name; + final VoidCallback onOpen; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const CircleAvatar( + backgroundColor: seedAvatar, + child: Icon(Icons.person, color: seedOnAvatar), + ), + title: Text(name), + trailing: trailing, + onTap: onOpen, + ); + } +} diff --git a/apps/app_seeds/pubspec.yaml b/apps/app_seeds/pubspec.yaml index 75ee26e..080e51e 100644 --- a/apps/app_seeds/pubspec.yaml +++ b/apps/app_seeds/pubspec.yaml @@ -130,9 +130,6 @@ flutter: # Seed strings for slang live under lib/i18n (not a Flutter asset; compiled). assets: - assets/catalog/species.json - # Web-of-trust bootstrap referents (Duniter "seeds"). Empty until real - # founding identities are curated; users bootstrap by adding their own. - - assets/trust/referents.json - assets/logo.png # Bundled so the Linux runner can use it as the GTK window icon. - assets/icon.png diff --git a/apps/app_seeds/test/services/trust_referents_test.dart b/apps/app_seeds/test/services/trust_referents_test.dart deleted file mode 100644 index a28cbed..0000000 --- a/apps/app_seeds/test/services/trust_referents_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:tane/services/trust_referents.dart'; - -import '../support/test_support.dart'; - -void main() { - const npub = - 'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6'; - const hex = - '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'; - - // No bundled asset in the unit test bundle → _bundled() degrades to empty, so - // these exercise the user-added set (the cold-start bootstrap path). - late TrustReferents referents; - setUp(() => referents = TrustReferents(InMemorySecretStore())); - - test('starts empty', () async { - expect(await referents.all(), isEmpty); - expect(await referents.userAdded(), isEmpty); - }); - - test('adds a referent from an npub, storing hex', () async { - final stored = await referents.add(npub); - expect(stored, hex); - expect(await referents.userAdded(), {hex}); - expect(await referents.all(), contains(hex)); - }); - - test('accepts a hex key directly and is idempotent', () async { - await referents.add(hex); - await referents.add(npub); // same identity, npub form - expect(await referents.userAdded(), {hex}); - }); - - test('rejects an invalid identity code', () async { - expect(() => referents.add('not-a-key'), throwsFormatException); - expect(await referents.userAdded(), isEmpty); - }); - - test('removes a referent', () async { - await referents.add(hex); - await referents.remove(hex); - expect(await referents.userAdded(), isEmpty); - }); -} diff --git a/apps/app_seeds/test/services/wot_settings_test.dart b/apps/app_seeds/test/services/wot_settings_test.dart deleted file mode 100644 index dbcc8fb..0000000 --- a/apps/app_seeds/test/services/wot_settings_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:commons_core/commons_core.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:tane/services/wot_settings.dart'; - -import '../support/test_support.dart'; - -void main() { - late WotSettings settings; - setUp(() => settings = WotSettings(InMemorySecretStore())); - - test('defaults to the Duniter parameters', () async { - expect(await settings.params(), WotParams.duniter); - }); - - test('persists saved parameters', () async { - const custom = WotParams( - sigQty: 3, stepMax: 4, sigValidity: Duration(days: 180)); - await settings.save(custom); - expect(await settings.params(), custom); - }); - - test('reset restores the Duniter defaults', () async { - await settings.save(const WotParams( - sigQty: 1, stepMax: 1, sigValidity: Duration(days: 30))); - await settings.reset(); - expect(await settings.params(), WotParams.duniter); - }); -} diff --git a/apps/app_seeds/test/state/trust_cubit_test.dart b/apps/app_seeds/test/state/trust_cubit_test.dart index d11e746..9dcb1f9 100644 --- a/apps/app_seeds/test/state/trust_cubit_test.dart +++ b/apps/app_seeds/test/state/trust_cubit_test.dart @@ -83,6 +83,7 @@ void main() { final inCircle = TrustCubit(transport, peerPubkey: peer, selfPubkey: me); await inCircle.load(); expect(inCircle.state.knownToYou, isTrue); + expect(inCircle.state.tier, TrustTier.inYourCircle); await inCircle.close(); final outside = TrustCubit(transport, peerPubkey: 'other', selfPubkey: me); @@ -91,39 +92,6 @@ void main() { await outside.close(); }); - test('network membership: enough referent vouches → member', () async { - const params = - WotParams(sigQty: 2, stepMax: 5, sigValidity: Duration(days: 365)); - final transport = FakeTrustTransport(me) - ..addCert('r1', peer) - ..addCert('r2', peer); - final cubit = TrustCubit( - transport, - peerPubkey: peer, - selfPubkey: me, - referents: {'r1', 'r2'}, - params: params, - ); - await cubit.load(); - expect(cubit.state.networkBootstrapped, isTrue); - expect(cubit.state.isNetworkMember, isTrue); - expect(cubit.state.tier, TrustTier.networkMember); - await cubit.close(); - }); - - test('with no referents, membership is undetermined (not a member)', - () async { - final transport = FakeTrustTransport(me) - ..addCert('r1', peer) - ..addCert('r2', peer); - // referents default to {} → the trust net isn't seeded. - final cubit = TrustCubit(transport, peerPubkey: peer, selfPubkey: me); - await cubit.load(); - expect(cubit.state.networkBootstrapped, isFalse); - expect(cubit.state.isNetworkMember, isFalse); - await cubit.close(); - }); - test('tier falls back to vouched, then unknown', () async { final vouched = TrustCubit( FakeTrustTransport(me)..addCert('someone', peer), diff --git a/apps/app_seeds/test/ui/your_people_screen_test.dart b/apps/app_seeds/test/ui/your_people_screen_test.dart new file mode 100644 index 0000000..00abf07 --- /dev/null +++ b/apps/app_seeds/test/ui/your_people_screen_test.dart @@ -0,0 +1,174 @@ +import 'package:commons_core/commons_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/i18n/strings.g.dart'; +import 'package:tane/services/profile_cache.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/ui/your_people_screen.dart'; + +import '../support/test_support.dart'; + +/// In-memory [TrustTransport] with a mutable certification graph. +class FakeTrustTransport implements TrustTransport { + FakeTrustTransport(this.selfId); + final String selfId; + final List certs = []; + + void addCert(String issuer, String subject) => certs.add(Certification( + issuer: issuer, + subject: subject, + issuedAt: DateTime(2026), + )); + + @override + Future> allCertifications() async => List.of(certs); + + @override + Future> certifiersOf(String subjectPubkey) async => { + for (final c in certs) + if (c.subject == subjectPubkey) c.issuer, + }; + + @override + Future certify({ + required String subjectPubkey, + Duration validity = const Duration(days: 365), + String note = '', + }) async => + addCert(selfId, subjectPubkey); + + @override + Future revoke({required String subjectPubkey}) async => certs + .removeWhere((c) => c.issuer == selfId && c.subject == subjectPubkey); + + @override + Future close() async {} +} + +/// A [SocialSession] stand-in that only carries the trust transport — the +/// screen under test never touches the other transports. +class FakeSession implements SocialSession { + FakeSession(this.trust); + + @override + final TrustTransport trust; + + @override + OfferTransport get offers => throw UnimplementedError(); + @override + MessageTransport get messages => throw UnimplementedError(); + @override + ProfileTransport get profile => throw UnimplementedError(); + @override + SyncTransport get sync => throw UnimplementedError(); + @override + Future close() async {} +} + +void main() { + const seedHex = + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'; + final peerA = 'aa' * 32; + final peerB = 'bb' * 32; + + late SocialService social; + + setUp(() async { + social = await SocialService.fromRootSeedHex(seedHex); + LocaleSettings.setLocaleSync(AppLocale.en); + }); + + Widget app(SocialConnection connection, {ProfileCache? cache}) => + TranslationProvider( + child: MaterialApp( + locale: AppLocale.en.flutterLocale, + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: YourPeopleScreen( + social: social, + connection: connection, + profileCache: cache, + ), + ), + ); + + SocialConnection online(TrustTransport trust) => SocialConnection( + social: social, + settings: SocialSettings(InMemorySecretStore()), + open: (_) async => FakeSession(trust) as SocialSession, + ); + + testWidgets('offline shows the friendly note', (tester) async { + final settings = SocialSettings(InMemorySecretStore()); + await settings.setRelayUrls(const []); // offline: don't hit the network + final connection = SocialConnection(social: social, settings: settings); + + await tester.pumpWidget(app(connection)); + await tester.pumpAndSettle(); + + expect( + find.text("You're offline — try again when you're connected"), + findsOneWidget, + ); + }); + + testWidgets('lists who you vouch for and who vouches for you, with names', + (tester) async { + final trust = FakeTrustTransport(social.publicKeyHex) + ..addCert(social.publicKeyHex, peerA) // you vouch for A + ..addCert(peerB, social.publicKeyHex) // B vouches for you + ..addCert(peerA, peerB); // unrelated edge, must not show + final cache = ProfileCache(InMemorySecretStore()); + await cache.setName(peerA, 'Alice'); + + await tester.pumpWidget(app(online(trust), cache: cache)); + await tester.pumpAndSettle(); + + expect(find.text('You vouch for'), findsOneWidget); + expect(find.text('They vouch for you'), findsOneWidget); + expect(find.text('Alice'), findsOneWidget); // cached name wins + expect(find.text(shortPubkey(peerB)), findsOneWidget); // fallback + expect(find.byKey(Key('yourPeople.given.$peerA')), findsOneWidget); + expect(find.byKey(Key('yourPeople.received.$peerB')), findsOneWidget); + }); + + testWidgets('empty states show gentle notes', (tester) async { + await tester + .pumpWidget(app(online(FakeTrustTransport(social.publicKeyHex)))); + await tester.pumpAndSettle(); + + expect( + find.textContaining("You don't vouch for anyone yet"), + findsOneWidget, + ); + expect(find.text('No one vouches for you yet'), findsOneWidget); + }); + + testWidgets('revoking removes the vouch after confirming', (tester) async { + final trust = FakeTrustTransport(social.publicKeyHex) + ..addCert(social.publicKeyHex, peerA); + + await tester.pumpWidget(app(online(trust))); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Stop vouching')); + await tester.pumpAndSettle(); + expect(find.text('Stop vouching for this person?'), findsOneWidget); + + await tester.tap(find.text('Stop vouching').last); // dialog action + await tester.pumpAndSettle(); + + expect(trust.certs, isEmpty); + expect( + find.textContaining("You don't vouch for anyone yet"), + findsOneWidget, + ); + }); +}