refactor(trust): ego-centric trust replaces the global Duniter membership

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.
This commit is contained in:
vjrj 2026-07-11 13:03:20 +02:00
parent a5f50314e4
commit fef174e8c4
25 changed files with 595 additions and 863 deletions

View file

@ -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": []
}

View file

@ -21,8 +21,6 @@ import 'services/social_account_store.dart';
import 'services/social_connection.dart'; import 'services/social_connection.dart';
import 'services/social_service.dart'; import 'services/social_service.dart';
import 'services/social_settings.dart'; import 'services/social_settings.dart';
import 'services/trust_referents.dart';
import 'services/wot_settings.dart';
import 'state/inventory_cubit.dart'; import 'state/inventory_cubit.dart';
import 'state/variety_detail_cubit.dart'; import 'state/variety_detail_cubit.dart';
import 'ui/about_screen.dart'; import 'ui/about_screen.dart';
@ -41,8 +39,8 @@ import 'ui/offline_banner.dart';
import 'ui/profile_screen.dart'; import 'ui/profile_screen.dart';
import 'ui/settings_screen.dart'; import 'ui/settings_screen.dart';
import 'ui/theme.dart'; import 'ui/theme.dart';
import 'ui/trust_network_screen.dart';
import 'ui/variety_detail_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: /// Root widget. Provides the repositories to the tree and wires go_router:
/// `/` is the home menu, `/inventory` the list, `/variety/:id` the item detail. /// `/` is the home menu, `/inventory` the list, `/variety/:id` the item detail.
@ -61,8 +59,6 @@ class TaneApp extends StatelessWidget {
this.profileStore, this.profileStore,
this.profileCache, this.profileCache,
this.socialAccounts, this.socialAccounts,
this.trustReferents,
this.wotSettings,
this.inbox, this.inbox,
this.notifications, this.notifications,
this.showIntro = false, this.showIntro = false,
@ -81,8 +77,6 @@ class TaneApp extends StatelessWidget {
profileStore, profileStore,
profileCache, profileCache,
socialAccounts, socialAccounts,
trustReferents,
wotSettings,
inbox, inbox,
) { ) {
// A tapped message notification opens that peer's chat. Wired here because // 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. /// Optional store of the active social identity, for the profile switcher.
final SocialAccountStore? socialAccounts; 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. /// App-wide inbox listener; drives the messages list's live refresh.
final InboxService? inbox; final InboxService? inbox;
@ -151,8 +141,6 @@ class TaneApp extends StatelessWidget {
ProfileStore? profileStore, ProfileStore? profileStore,
ProfileCache? profileCache, ProfileCache? profileCache,
SocialAccountStore? socialAccounts, SocialAccountStore? socialAccounts,
TrustReferents? trustReferents,
WotSettings? wotSettings,
InboxService? inbox, InboxService? inbox,
) { ) {
return GoRouter( return GoRouter(
@ -208,16 +196,16 @@ class TaneApp extends StatelessWidget {
connection: connection, connection: connection,
profileStore: profileStore, profileStore: profileStore,
accounts: socialAccounts, accounts: socialAccounts,
trustNetworkEnabled: yourPeopleEnabled: true,
trustReferents != null && wotSettings != null,
), ),
), ),
if (trustReferents != null && wotSettings != null) if (social != null && connection != null)
GoRoute( GoRoute(
path: '/trust', path: '/your-people',
builder: (context, state) => TrustNetworkScreen( builder: (context, state) => YourPeopleScreen(
referents: trustReferents, social: social,
wotSettings: wotSettings, connection: connection,
profileCache: profileCache,
), ),
), ),
if (social != null && connection != null) if (social != null && connection != null)
@ -229,8 +217,6 @@ class TaneApp extends StatelessWidget {
peerPubkey: state.pathParameters['pubkey']!, peerPubkey: state.pathParameters['pubkey']!,
messageStore: messageStore, messageStore: messageStore,
profileCache: profileCache, profileCache: profileCache,
trustReferents: trustReferents,
wotSettings: wotSettings,
), ),
), ),
GoRoute( GoRoute(

View file

@ -23,8 +23,6 @@ import 'services/social_connection.dart';
import 'services/social_service.dart'; import 'services/social_service.dart';
import 'services/social_settings.dart'; import 'services/social_settings.dart';
import 'services/sync_service.dart'; import 'services/sync_service.dart';
import 'services/trust_referents.dart';
import 'services/wot_settings.dart';
import 'ui/theme.dart'; import 'ui/theme.dart';
/// Boots the app WITHOUT blocking the first frame: paints a splash immediately, /// Boots the app WITHOUT blocking the first frame: paints a splash immediately,
@ -88,8 +86,6 @@ class _BootstrapState extends State<Bootstrap> {
profileStore: getIt<ProfileStore>(), profileStore: getIt<ProfileStore>(),
profileCache: getIt<ProfileCache>(), profileCache: getIt<ProfileCache>(),
socialAccounts: getIt<SocialAccountStore>(), socialAccounts: getIt<SocialAccountStore>(),
trustReferents: getIt<TrustReferents>(),
wotSettings: getIt<WotSettings>(),
inbox: inbox, inbox: inbox,
notifications: notifications, notifications: notifications,
showIntro: !await onboarding.introSeen(), showIntro: !await onboarding.introSeen(),

View file

@ -43,9 +43,7 @@ import '../services/social_connection.dart';
import '../services/social_service.dart'; import '../services/social_service.dart';
import '../services/social_settings.dart'; import '../services/social_settings.dart';
import '../services/sync_service.dart'; import '../services/sync_service.dart';
import '../services/trust_referents.dart';
import '../services/unread_service.dart'; import '../services/unread_service.dart';
import '../services/wot_settings.dart';
/// The app's service locator. Kept to the composition root — widgets get their /// 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 /// repositories from here (or via BlocProvider), never by reaching into it deep
@ -170,9 +168,6 @@ Future<void> configureDependencies() async {
..registerSingleton<LocaleStore>(LocaleStore(secretStore)) ..registerSingleton<LocaleStore>(LocaleStore(secretStore))
..registerSingleton<SocialSettings>(SocialSettings(secretStore)) ..registerSingleton<SocialSettings>(SocialSettings(secretStore))
..registerSingleton<SocialAccountStore>(accounts) ..registerSingleton<SocialAccountStore>(accounts)
// Web of trust: network-wide (not per-identity), so registered once.
..registerSingleton<TrustReferents>(TrustReferents(secretStore))
..registerSingleton<WotSettings>(WotSettings(secretStore))
..registerSingleton<OfferOutbox>(OfferOutbox(secretStore)) ..registerSingleton<OfferOutbox>(OfferOutbox(secretStore))
// Per-identity stores are namespaced by the active account's scope. // Per-identity stores are namespaced by the active account's scope.
..registerSingleton<MessageStore>( ..registerSingleton<MessageStore>(

View file

@ -537,29 +537,18 @@
"count": "Avalada por {n}", "count": "Avalada por {n}",
"vouch": "Conozo a esta persona", "vouch": "Conozo a esta persona",
"vouched": "Avales a esta persona", "vouched": "Avales a esta persona",
"circle": "Nel to círculu", "circle": "Nel to círculu"
"member": "Miembru de confianza de la rede"
}, },
"wot": { "yourPeople": {
"title": "Rede de confianza", "title": "La to xente",
"open": "Rede de confianza", "help": "Persones que conoces y avales, y persones que t'avalen.",
"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.", "youVouchFor": "Tu avales a",
"roots": "Raíces de confianza", "vouchesForYou": "Aválente",
"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.", "youVouchForEmpty": "Entá nun avales a naide. Cuando conozas a daquién, abri la vuestra conversación y calca \"Conozo a esta persona\".",
"noRoots": "Entá nun hai raíces de confianza — nun se pue calcular la pertenencia hasta semar la rede.", "vouchesForYouEmpty": "Naide t'avala entá",
"addRoot": "Amestar una raíz de confianza", "revoke": "Dexar d'avalar",
"rootHint": "Apega un códigu d'identidá (npub…)", "revokeConfirm": "¿Dexar d'avalar a esta persona?",
"add": "Amestar", "offline": "Ensin conexón — vuelvi tentalo cuando teas en llinia"
"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"
}, },
"notifications": { "notifications": {
"newMessageFrom": "Mensaxe nuevu de {name}" "newMessageFrom": "Mensaxe nuevu de {name}"

View file

@ -540,29 +540,18 @@
"count": "Vouched for by {n}", "count": "Vouched for by {n}",
"vouch": "I know this person", "vouch": "I know this person",
"vouched": "You vouch for them", "vouched": "You vouch for them",
"circle": "In your circle", "circle": "In your circle"
"member": "Trusted member of the network"
}, },
"wot": { "yourPeople": {
"title": "Network of trust", "title": "Your people",
"open": "Network of trust", "help": "People you've met and vouch for, and people who vouch for you.",
"help": "How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.", "youVouchFor": "You vouch for",
"roots": "Trust roots", "vouchesForYou": "They vouch for you",
"rootsHelp": "The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.", "youVouchForEmpty": "You don't vouch for anyone yet. When you meet someone, open your chat with them and tap \"I know this person\".",
"noRoots": "No trust roots yet — membership can't be worked out until the network is seeded.", "vouchesForYouEmpty": "No one vouches for you yet",
"addRoot": "Add a trust root", "revoke": "Stop vouching",
"rootHint": "Paste an identity code (npub…)", "revokeConfirm": "Stop vouching for this person?",
"add": "Add", "offline": "You're offline — try again when you're connected"
"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"
}, },
"notifications": { "notifications": {
"newMessageFrom": "New message from {name}" "newMessageFrom": "New message from {name}"

View file

@ -539,29 +539,18 @@
"count": "Avalada por {n}", "count": "Avalada por {n}",
"vouch": "Conozco a esta persona", "vouch": "Conozco a esta persona",
"vouched": "Avalas a esta persona", "vouched": "Avalas a esta persona",
"circle": "En tu círculo", "circle": "En tu círculo"
"member": "Miembro de confianza de la red"
}, },
"wot": { "yourPeople": {
"title": "Red de confianza", "title": "Tu gente",
"open": "Red de confianza", "help": "Personas que conoces y avalas, y personas que te avalan.",
"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.", "youVouchFor": "Tú avalas a",
"roots": "Raíces de confianza", "vouchesForYou": "Te avalan",
"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.", "youVouchForEmpty": "Aún no avalas a nadie. Cuando conozcas a alguien, abre vuestro chat y toca \"Conozco a esta persona\".",
"noRoots": "Aún no hay raíces de confianza — no se puede calcular la membresía hasta sembrar la red.", "vouchesForYouEmpty": "Nadie te avala todavía",
"addRoot": "Añadir una raíz de confianza", "revoke": "Dejar de avalar",
"rootHint": "Pega un código de identidad (npub…)", "revokeConfirm": "¿Dejar de avalar a esta persona?",
"add": "Añadir", "offline": "Sin conexión — inténtalo cuando estés en línea"
"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"
}, },
"notifications": { "notifications": {
"newMessageFrom": "Nuevo mensaje de {name}" "newMessageFrom": "Nuevo mensaje de {name}"

View file

@ -536,29 +536,18 @@
"count": "Avalizada por {n}", "count": "Avalizada por {n}",
"vouch": "Conheço esta pessoa", "vouch": "Conheço esta pessoa",
"vouched": "Avalizas esta pessoa", "vouched": "Avalizas esta pessoa",
"circle": "No teu círculo", "circle": "No teu círculo"
"member": "Membro de confiança da rede"
}, },
"wot": { "yourPeople": {
"title": "Rede de confiança", "title": "A tua gente",
"open": "Rede de confiança", "help": "Pessoas que conheces e avalizas, e pessoas que te avalizam.",
"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.", "youVouchFor": "Tu avalizas",
"roots": "Raízes de confiança", "vouchesForYou": "Avalizam-te",
"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.", "youVouchForEmpty": "Ainda não avalizas ninguém. Quando conheceres alguém, abre a vossa conversa e toca em \"Conheço esta pessoa\".",
"noRoots": "Ainda não há raízes de confiança — não se pode calcular a filiação até semear a rede.", "vouchesForYouEmpty": "Ainda ninguém te avaliza",
"addRoot": "Adicionar uma raiz de confiança", "revoke": "Deixar de avalizar",
"rootHint": "Cola um código de identidade (npub…)", "revokeConfirm": "Deixar de avalizar esta pessoa?",
"add": "Adicionar", "offline": "Sem ligação — tenta novamente quando estiveres em linha"
"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"
}, },
"notifications": { "notifications": {
"newMessageFrom": "Nova mensagem de {name}" "newMessageFrom": "Nova mensagem de {name}"

View file

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 4 /// 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 // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import

View file

@ -76,7 +76,7 @@ class TranslationsAst extends Translations with BaseTranslations<AppLocale, Tran
@override late final _Translations$chatList$ast chatList = _Translations$chatList$ast._(_root); @override late final _Translations$chatList$ast chatList = _Translations$chatList$ast._(_root);
@override late final _Translations$chat$ast chat = _Translations$chat$ast._(_root); @override late final _Translations$chat$ast chat = _Translations$chat$ast._(_root);
@override late final _Translations$trust$ast trust = _Translations$trust$ast._(_root); @override late final _Translations$trust$ast trust = _Translations$trust$ast._(_root);
@override late final _Translations$wot$ast wot = _Translations$wot$ast._(_root); @override late final _Translations$yourPeople$ast yourPeople = _Translations$yourPeople$ast._(_root);
@override late final _Translations$notifications$ast notifications = _Translations$notifications$ast._(_root); @override late final _Translations$notifications$ast notifications = _Translations$notifications$ast._(_root);
@override late final _Translations$plantare$ast plantare = _Translations$plantare$ast._(_root); @override late final _Translations$plantare$ast plantare = _Translations$plantare$ast._(_root);
@override late final _Translations$sale$ast sale = _Translations$sale$ast._(_root); @override late final _Translations$sale$ast sale = _Translations$sale$ast._(_root);
@ -786,35 +786,24 @@ class _Translations$trust$ast extends Translations$trust$en {
@override String get vouch => 'Conozo a esta persona'; @override String get vouch => 'Conozo a esta persona';
@override String get vouched => 'Avales a esta persona'; @override String get vouched => 'Avales a esta persona';
@override String get circle => 'Nel to círculu'; @override String get circle => 'Nel to círculu';
@override String get member => 'Miembru de confianza de la rede';
} }
// Path: wot // Path: yourPeople
class _Translations$wot$ast extends Translations$wot$en { class _Translations$yourPeople$ast extends Translations$yourPeople$en {
_Translations$wot$ast._(TranslationsAst root) : this._root = root, super.internal(root); _Translations$yourPeople$ast._(TranslationsAst root) : this._root = root, super.internal(root);
final TranslationsAst _root; // ignore: unused_field final TranslationsAst _root; // ignore: unused_field
// Translations // Translations
@override String get title => 'Rede de confianza'; @override String get title => 'La to xente';
@override String get open => 'Rede de confianza'; @override String get help => 'Persones que conoces y avales, y persones que t\'avalen.';
@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 youVouchFor => 'Tu avales a';
@override String get roots => 'Raíces de confianza'; @override String get vouchesForYou => 'Aválente';
@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 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 noRoots => 'Entá nun hai raíces de confianza — nun se pue calcular la pertenencia hasta semar la rede.'; @override String get vouchesForYouEmpty => 'Naide t\'avala entá';
@override String get addRoot => 'Amestar una raíz de confianza'; @override String get revoke => 'Dexar d\'avalar';
@override String get rootHint => 'Apega un códigu d\'identidá (npub…)'; @override String get revokeConfirm => '¿Dexar d\'avalar a esta persona?';
@override String get add => 'Amestar'; @override String get offline => 'Ensin conexón — vuelvi tentalo cuando teas en llinia';
@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';
} }
// Path: notifications // Path: notifications
@ -1629,26 +1618,15 @@ extension on TranslationsAst {
'trust.vouch' => 'Conozo a esta persona', 'trust.vouch' => 'Conozo a esta persona',
'trust.vouched' => 'Avales a esta persona', 'trust.vouched' => 'Avales a esta persona',
'trust.circle' => 'Nel to círculu', 'trust.circle' => 'Nel to círculu',
'trust.member' => 'Miembru de confianza de la rede', 'yourPeople.title' => 'La to xente',
'wot.title' => 'Rede de confianza', 'yourPeople.help' => 'Persones que conoces y avales, y persones que t\'avalen.',
'wot.open' => 'Rede de confianza', 'yourPeople.youVouchFor' => 'Tu avales a',
'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.', 'yourPeople.vouchesForYou' => 'Aválente',
'wot.roots' => 'Raíces de confianza', 'yourPeople.youVouchForEmpty' => 'Entá nun avales a naide. Cuando conozas a daquién, abri la vuestra conversación y calca "Conozo a esta persona".',
'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.', 'yourPeople.vouchesForYouEmpty' => 'Naide t\'avala entá',
'wot.noRoots' => 'Entá nun hai raíces de confianza — nun se pue calcular la pertenencia hasta semar la rede.', 'yourPeople.revoke' => 'Dexar d\'avalar',
'wot.addRoot' => 'Amestar una raíz de confianza', 'yourPeople.revokeConfirm' => '¿Dexar d\'avalar a esta persona?',
'wot.rootHint' => 'Apega un códigu d\'identidá (npub…)', 'yourPeople.offline' => 'Ensin conexón — vuelvi tentalo cuando teas en llinia',
'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',
'notifications.newMessageFrom' => ({required Object name}) => 'Mensaxe nuevu de ${name}', 'notifications.newMessageFrom' => ({required Object name}) => 'Mensaxe nuevu de ${name}',
'plantare.title' => 'Plantares', '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.', '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.',

View file

@ -77,7 +77,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
late final Translations$chatList$en chatList = Translations$chatList$en.internal(_root); 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$chat$en chat = Translations$chat$en.internal(_root);
late final Translations$trust$en trust = Translations$trust$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$notifications$en notifications = Translations$notifications$en.internal(_root);
late final Translations$plantare$en plantare = Translations$plantare$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); late final Translations$sale$en sale = Translations$sale$en.internal(_root);
@ -1467,75 +1467,42 @@ class Translations$trust$en {
/// en: 'In your circle' /// en: 'In your circle'
String get circle => '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 // Path: yourPeople
class Translations$wot$en { class Translations$yourPeople$en {
Translations$wot$en.internal(this._root); Translations$yourPeople$en.internal(this._root);
final Translations _root; // ignore: unused_field final Translations _root; // ignore: unused_field
// Translations // Translations
/// en: 'Network of trust' /// en: 'Your people'
String get title => 'Network of trust'; String get title => 'Your people';
/// en: 'Network of trust' /// en: 'People you've met and vouch for, and people who vouch for you.'
String get open => 'Network of trust'; 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.' /// en: 'You vouch for'
String get help => 'How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.'; String get youVouchFor => 'You vouch for';
/// en: 'Trust roots' /// en: 'They vouch for you'
String get roots => 'Trust roots'; 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.' /// 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 rootsHelp => 'The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.'; 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.' /// en: 'No one vouches for you yet'
String get noRoots => 'No trust roots yet — membership can\'t be worked out until the network is seeded.'; String get vouchesForYouEmpty => 'No one vouches for you yet';
/// en: 'Add a trust root' /// en: 'Stop vouching'
String get addRoot => 'Add a trust root'; String get revoke => 'Stop vouching';
/// en: 'Paste an identity code (npub…)' /// en: 'Stop vouching for this person?'
String get rootHint => 'Paste an identity code (npub…)'; String get revokeConfirm => 'Stop vouching for this person?';
/// en: 'Add' /// en: 'You're offline try again when you're connected'
String get add => 'Add'; String get offline => 'You\'re offline — try again when you\'re connected';
/// 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';
} }
// Path: notifications // Path: notifications
@ -2549,26 +2516,15 @@ extension on Translations {
'trust.vouch' => 'I know this person', 'trust.vouch' => 'I know this person',
'trust.vouched' => 'You vouch for them', 'trust.vouched' => 'You vouch for them',
'trust.circle' => 'In your circle', 'trust.circle' => 'In your circle',
'trust.member' => 'Trusted member of the network', 'yourPeople.title' => 'Your people',
'wot.title' => 'Network of trust', 'yourPeople.help' => 'People you\'ve met and vouch for, and people who vouch for you.',
'wot.open' => 'Network of trust', 'yourPeople.youVouchFor' => 'You vouch for',
'wot.help' => 'How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.', 'yourPeople.vouchesForYou' => 'They vouch for you',
'wot.roots' => 'Trust roots', 'yourPeople.youVouchForEmpty' => 'You don\'t vouch for anyone yet. When you meet someone, open your chat with them and tap "I know this person".',
'wot.rootsHelp' => 'The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.', 'yourPeople.vouchesForYouEmpty' => 'No one vouches for you yet',
'wot.noRoots' => 'No trust roots yet — membership can\'t be worked out until the network is seeded.', 'yourPeople.revoke' => 'Stop vouching',
'wot.addRoot' => 'Add a trust root', 'yourPeople.revokeConfirm' => 'Stop vouching for this person?',
'wot.rootHint' => 'Paste an identity code (npub…)', 'yourPeople.offline' => 'You\'re offline — try again when you\'re connected',
'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',
'notifications.newMessageFrom' => ({required Object name}) => 'New message from ${name}', 'notifications.newMessageFrom' => ({required Object name}) => 'New message from ${name}',
'plantare.title' => 'Plantares', '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.', '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.',

View file

@ -76,7 +76,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$chatList$es chatList = _Translations$chatList$es._(_root); @override late final _Translations$chatList$es chatList = _Translations$chatList$es._(_root);
@override late final _Translations$chat$es chat = _Translations$chat$es._(_root); @override late final _Translations$chat$es chat = _Translations$chat$es._(_root);
@override late final _Translations$trust$es trust = _Translations$trust$es._(_root); @override late final _Translations$trust$es trust = _Translations$trust$es._(_root);
@override late final _Translations$wot$es wot = _Translations$wot$es._(_root); @override late final _Translations$yourPeople$es yourPeople = _Translations$yourPeople$es._(_root);
@override late final _Translations$notifications$es notifications = _Translations$notifications$es._(_root); @override late final _Translations$notifications$es notifications = _Translations$notifications$es._(_root);
@override late final _Translations$plantare$es plantare = _Translations$plantare$es._(_root); @override late final _Translations$plantare$es plantare = _Translations$plantare$es._(_root);
@override late final _Translations$sale$es sale = _Translations$sale$es._(_root); @override late final _Translations$sale$es sale = _Translations$sale$es._(_root);
@ -788,35 +788,24 @@ class _Translations$trust$es extends Translations$trust$en {
@override String get vouch => 'Conozco a esta persona'; @override String get vouch => 'Conozco a esta persona';
@override String get vouched => 'Avalas a esta persona'; @override String get vouched => 'Avalas a esta persona';
@override String get circle => 'En tu círculo'; @override String get circle => 'En tu círculo';
@override String get member => 'Miembro de confianza de la red';
} }
// Path: wot // Path: yourPeople
class _Translations$wot$es extends Translations$wot$en { class _Translations$yourPeople$es extends Translations$yourPeople$en {
_Translations$wot$es._(TranslationsEs root) : this._root = root, super.internal(root); _Translations$yourPeople$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field final TranslationsEs _root; // ignore: unused_field
// Translations // Translations
@override String get title => 'Red de confianza'; @override String get title => 'Tu gente';
@override String get open => 'Red de confianza'; @override String get help => 'Personas que conoces y avalas, y personas que te avalan.';
@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 youVouchFor => 'Tú avalas a';
@override String get roots => 'Raíces de confianza'; @override String get vouchesForYou => 'Te avalan';
@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 youVouchForEmpty => 'Aún no avalas a nadie. Cuando conozcas a alguien, abre vuestro chat y toca "Conozco a esta persona".';
@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 vouchesForYouEmpty => 'Nadie te avala todavía';
@override String get addRoot => 'Añadir una raíz de confianza'; @override String get revoke => 'Dejar de avalar';
@override String get rootHint => 'Pega un código de identidad (npub…)'; @override String get revokeConfirm => '¿Dejar de avalar a esta persona?';
@override String get add => 'Añadir'; @override String get offline => 'Sin conexión — inténtalo cuando estés en línea';
@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';
} }
// Path: notifications // Path: notifications
@ -1633,26 +1622,15 @@ extension on TranslationsEs {
'trust.vouch' => 'Conozco a esta persona', 'trust.vouch' => 'Conozco a esta persona',
'trust.vouched' => 'Avalas a esta persona', 'trust.vouched' => 'Avalas a esta persona',
'trust.circle' => 'En tu círculo', 'trust.circle' => 'En tu círculo',
'trust.member' => 'Miembro de confianza de la red', 'yourPeople.title' => 'Tu gente',
'wot.title' => 'Red de confianza', 'yourPeople.help' => 'Personas que conoces y avalas, y personas que te avalan.',
'wot.open' => 'Red de confianza', 'yourPeople.youVouchFor' => 'Tú avalas a',
'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.', 'yourPeople.vouchesForYou' => 'Te avalan',
'wot.roots' => 'Raíces de confianza', 'yourPeople.youVouchForEmpty' => 'Aún no avalas a nadie. Cuando conozcas a alguien, abre vuestro chat y toca "Conozco a esta persona".',
'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.', 'yourPeople.vouchesForYouEmpty' => 'Nadie te avala todavía',
'wot.noRoots' => 'Aún no hay raíces de confianza — no se puede calcular la membresía hasta sembrar la red.', 'yourPeople.revoke' => 'Dejar de avalar',
'wot.addRoot' => 'Añadir una raíz de confianza', 'yourPeople.revokeConfirm' => '¿Dejar de avalar a esta persona?',
'wot.rootHint' => 'Pega un código de identidad (npub…)', 'yourPeople.offline' => 'Sin conexión — inténtalo cuando estés en línea',
'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',
'notifications.newMessageFrom' => ({required Object name}) => 'Nuevo mensaje de ${name}', 'notifications.newMessageFrom' => ({required Object name}) => 'Nuevo mensaje de ${name}',
'plantare.title' => 'Plantares', '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.', '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.',

View file

@ -76,7 +76,7 @@ class TranslationsPt extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$chatList$pt chatList = _Translations$chatList$pt._(_root); @override late final _Translations$chatList$pt chatList = _Translations$chatList$pt._(_root);
@override late final _Translations$chat$pt chat = _Translations$chat$pt._(_root); @override late final _Translations$chat$pt chat = _Translations$chat$pt._(_root);
@override late final _Translations$trust$pt trust = _Translations$trust$pt._(_root); @override late final _Translations$trust$pt trust = _Translations$trust$pt._(_root);
@override late final _Translations$wot$pt wot = _Translations$wot$pt._(_root); @override late final _Translations$yourPeople$pt yourPeople = _Translations$yourPeople$pt._(_root);
@override late final _Translations$notifications$pt notifications = _Translations$notifications$pt._(_root); @override late final _Translations$notifications$pt notifications = _Translations$notifications$pt._(_root);
@override late final _Translations$plantare$pt plantare = _Translations$plantare$pt._(_root); @override late final _Translations$plantare$pt plantare = _Translations$plantare$pt._(_root);
@override late final _Translations$sale$pt sale = _Translations$sale$pt._(_root); @override late final _Translations$sale$pt sale = _Translations$sale$pt._(_root);
@ -785,35 +785,24 @@ class _Translations$trust$pt extends Translations$trust$en {
@override String get vouch => 'Conheço esta pessoa'; @override String get vouch => 'Conheço esta pessoa';
@override String get vouched => 'Avalizas esta pessoa'; @override String get vouched => 'Avalizas esta pessoa';
@override String get circle => 'No teu círculo'; @override String get circle => 'No teu círculo';
@override String get member => 'Membro de confiança da rede';
} }
// Path: wot // Path: yourPeople
class _Translations$wot$pt extends Translations$wot$en { class _Translations$yourPeople$pt extends Translations$yourPeople$en {
_Translations$wot$pt._(TranslationsPt root) : this._root = root, super.internal(root); _Translations$yourPeople$pt._(TranslationsPt root) : this._root = root, super.internal(root);
final TranslationsPt _root; // ignore: unused_field final TranslationsPt _root; // ignore: unused_field
// Translations // Translations
@override String get title => 'Rede de confiança'; @override String get title => 'A tua gente';
@override String get open => 'Rede de confiança'; @override String get help => 'Pessoas que conheces e avalizas, e pessoas que te avalizam.';
@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 youVouchFor => 'Tu avalizas';
@override String get roots => 'Raízes de confiança'; @override String get vouchesForYou => 'Avalizam-te';
@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 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 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 vouchesForYouEmpty => 'Ainda ninguém te avaliza';
@override String get addRoot => 'Adicionar uma raiz de confiança'; @override String get revoke => 'Deixar de avalizar';
@override String get rootHint => 'Cola um código de identidade (npub…)'; @override String get revokeConfirm => 'Deixar de avalizar esta pessoa?';
@override String get add => 'Adicionar'; @override String get offline => 'Sem ligação — tenta novamente quando estiveres em linha';
@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';
} }
// Path: notifications // Path: notifications
@ -1627,26 +1616,15 @@ extension on TranslationsPt {
'trust.vouch' => 'Conheço esta pessoa', 'trust.vouch' => 'Conheço esta pessoa',
'trust.vouched' => 'Avalizas esta pessoa', 'trust.vouched' => 'Avalizas esta pessoa',
'trust.circle' => 'No teu círculo', 'trust.circle' => 'No teu círculo',
'trust.member' => 'Membro de confiança da rede', 'yourPeople.title' => 'A tua gente',
'wot.title' => 'Rede de confiança', 'yourPeople.help' => 'Pessoas que conheces e avalizas, e pessoas que te avalizam.',
'wot.open' => 'Rede de confiança', 'yourPeople.youVouchFor' => 'Tu avalizas',
'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.', 'yourPeople.vouchesForYou' => 'Avalizam-te',
'wot.roots' => 'Raízes de confiança', 'yourPeople.youVouchForEmpty' => 'Ainda não avalizas ninguém. Quando conheceres alguém, abre a vossa conversa e toca em "Conheço esta pessoa".',
'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.', 'yourPeople.vouchesForYouEmpty' => 'Ainda ninguém te avaliza',
'wot.noRoots' => 'Ainda não há raízes de confiança — não se pode calcular a filiação até semear a rede.', 'yourPeople.revoke' => 'Deixar de avalizar',
'wot.addRoot' => 'Adicionar uma raiz de confiança', 'yourPeople.revokeConfirm' => 'Deixar de avalizar esta pessoa?',
'wot.rootHint' => 'Cola um código de identidade (npub…)', 'yourPeople.offline' => 'Sem ligação — tenta novamente quando estiveres em linha',
'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',
'notifications.newMessageFrom' => ({required Object name}) => 'Nova mensagem de ${name}', 'notifications.newMessageFrom' => ({required Object name}) => 'Nova mensagem de ${name}',
'plantare.title' => 'Plantares', '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.', '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.',

View file

@ -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<String>? _bundledCache;
/// Every referent (bundled user-added), as hex public keys.
Future<Set<String>> all() async =>
{...await _bundled(), ...await userAdded()};
/// Referents the user added themselves (hex), for the management UI.
Future<Set<String>> 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<String> 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<void> remove(String hex) async {
final current = await userAdded()..remove(hex);
await _store.write(_userKey, current.join('\n'));
}
Future<List<String>> _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 = <String>[];
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
}
}
}

View file

@ -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<WotParams> 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<void> 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<void> reset() => save(WotParams.duniter);
Future<int?> _readInt(String key) async {
final raw = await _store.read(key);
if (raw == null || raw.isEmpty) return null;
return int.tryParse(raw);
}
}

View file

@ -2,32 +2,28 @@ import 'package:commons_core/commons_core.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.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 { 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). /// In your personal circle (you vouch, or a friend-of-a-friend does).
inYourCircle, 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, vouched,
/// No certifications seen. /// No certifications seen.
unknown, unknown,
} }
/// Trust standing of one peer: the full Duniter membership verdict, your own /// Trust standing of one peer, seen from this user's position: your own
/// circle, and the raw certifier count computed from the public certification /// vouch, your circle (friend-of-a-friend), and the raw certifier count
/// graph with the active [WotParams] and bootstrap referents. /// computed from the public certification graph.
class TrustState extends Equatable { class TrustState extends Equatable {
const TrustState({ const TrustState({
this.certifierCount = 0, this.certifierCount = 0,
this.iVouch = false, this.iVouch = false,
this.knownToYou = false, this.knownToYou = false,
this.isNetworkMember = false,
this.networkBootstrapped = false,
this.loading = true, this.loading = true,
this.busy = false, this.busy = false,
}); });
@ -42,21 +38,11 @@ class TrustState extends Equatable {
/// friend-of-a-friend, vouch). Spam-resistant and works from day one. /// friend-of-a-friend, vouch). Spam-resistant and works from day one.
final bool knownToYou; 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 loading;
final bool busy; final bool busy;
/// The strongest applicable signal, for the badge. /// The strongest applicable signal, for the badge.
TrustTier get tier { TrustTier get tier {
if (isNetworkMember) return TrustTier.networkMember;
if (knownToYou) return TrustTier.inYourCircle; if (knownToYou) return TrustTier.inYourCircle;
if (certifierCount > 0) return TrustTier.vouched; if (certifierCount > 0) return TrustTier.vouched;
return TrustTier.unknown; return TrustTier.unknown;
@ -66,8 +52,6 @@ class TrustState extends Equatable {
int? certifierCount, int? certifierCount,
bool? iVouch, bool? iVouch,
bool? knownToYou, bool? knownToYou,
bool? isNetworkMember,
bool? networkBootstrapped,
bool? loading, bool? loading,
bool? busy, bool? busy,
}) => }) =>
@ -75,8 +59,6 @@ class TrustState extends Equatable {
certifierCount: certifierCount ?? this.certifierCount, certifierCount: certifierCount ?? this.certifierCount,
iVouch: iVouch ?? this.iVouch, iVouch: iVouch ?? this.iVouch,
knownToYou: knownToYou ?? this.knownToYou, knownToYou: knownToYou ?? this.knownToYou,
isNetworkMember: isNetworkMember ?? this.isNetworkMember,
networkBootstrapped: networkBootstrapped ?? this.networkBootstrapped,
loading: loading ?? this.loading, loading: loading ?? this.loading,
busy: busy ?? this.busy, busy: busy ?? this.busy,
); );
@ -86,23 +68,19 @@ class TrustState extends Equatable {
certifierCount, certifierCount,
iVouch, iVouch,
knownToYou, knownToYou,
isNetworkMember,
networkBootstrapped,
loading, loading,
busy, busy,
]; ];
} }
/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport], /// Reads and toggles this user's vouch for [peerPubkey] over a
/// and computes the full Duniter membership verdict against the bootstrap /// [TrustTransport], and computes the peer's standing from this user's own
/// [referents] and active [params]. /// position in the vouch graph (ego-centric no referents, no parameters).
class TrustCubit extends Cubit<TrustState> { class TrustCubit extends Cubit<TrustState> {
TrustCubit( TrustCubit(
this._transport, { this._transport, {
required this.peerPubkey, required this.peerPubkey,
required this.selfPubkey, required this.selfPubkey,
this.referents = const {},
this.params = WotParams.duniter,
Future<void> Function()? onDispose, Future<void> Function()? onDispose,
}) : _onDispose = onDispose, }) : _onDispose = onDispose,
super(const TrustState()); super(const TrustState());
@ -110,26 +88,20 @@ class TrustCubit extends Cubit<TrustState> {
final TrustTransport? _transport; final TrustTransport? _transport;
final String peerPubkey; final String peerPubkey;
final String selfPubkey; final String selfPubkey;
/// Bootstrap referents (Duniter seeds) the membership rule trusts by
/// construction. Empty = the network isn't seeded yet.
final Set<String> referents;
/// Active web-of-trust parameters (sigQty / stepMax / validity).
final WotParams params;
final Future<void> Function()? _onDispose; final Future<void> Function()? _onDispose;
bool get isOnline => _transport != null; bool get isOnline => _transport != null;
/// Personal "circle" rule: one vouch from your side, out to a friend-of-a- /// Personal "circle" rule: one vouch from your side, out to a friend-of-a-
/// friend. Loose by design "people near you", separate from formal /// friend. Loose by design "people near you".
/// membership.
static const _circleThreshold = 1; static const _circleThreshold = 1;
static const _circleDistance = 2; 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, /// Loads the certification graph and computes: the peer's certifier count,
/// whether you vouch, whether they're in your circle, and whether they pass /// whether you vouch, and whether they're in your circle.
/// the full Duniter membership rule against the bootstrap referents.
Future<void> load() async { Future<void> load() async {
final transport = _transport; final transport = _transport;
if (transport == null) { if (transport == null) {
@ -147,23 +119,17 @@ class TrustCubit extends Cubit<TrustState> {
threshold: _circleThreshold, threshold: _circleThreshold,
maxDistance: _circleDistance, maxDistance: _circleDistance,
); );
// Full membership: only meaningful once the trust net has referents.
final members = referents.isEmpty
? const <String>{}
: wot.membersWith(seeds: referents, params: params);
emit(state.copyWith( emit(state.copyWith(
certifierCount: certifiers.length, certifierCount: certifiers.length,
iVouch: certifiers.contains(selfPubkey), iVouch: certifiers.contains(selfPubkey),
knownToYou: circle.contains(peerPubkey), knownToYou: circle.contains(peerPubkey),
isNetworkMember: members.contains(peerPubkey),
networkBootstrapped: referents.isNotEmpty,
loading: false, loading: false,
)); ));
} }
/// Adds or removes this user's vouch, then reloads. Never vouches for self. /// 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 /// The certification is issued with a validity so it expires and must be
/// must be renewed (Duniter rule). /// renewed.
Future<void> toggleVouch() async { Future<void> toggleVouch() async {
final transport = _transport; final transport = _transport;
if (transport == null || peerPubkey == selfPubkey) return; if (transport == null || peerPubkey == selfPubkey) return;
@ -173,7 +139,7 @@ class TrustCubit extends Cubit<TrustState> {
} else { } else {
await transport.certify( await transport.certify(
subjectPubkey: peerPubkey, subjectPubkey: peerPubkey,
validity: params.sigValidity, validity: _vouchValidity,
); );
} }
await load(); await load();

View file

@ -13,9 +13,7 @@ import '../services/message_store.dart';
import '../services/profile_cache.dart'; import '../services/profile_cache.dart';
import '../services/social_connection.dart'; import '../services/social_connection.dart';
import '../services/social_service.dart'; import '../services/social_service.dart';
import '../services/trust_referents.dart';
import '../services/unread_service.dart'; import '../services/unread_service.dart';
import '../services/wot_settings.dart';
import '../state/messages_cubit.dart'; import '../state/messages_cubit.dart';
import '../state/trust_cubit.dart'; import '../state/trust_cubit.dart';
import 'peer_avatar.dart'; import 'peer_avatar.dart';
@ -31,8 +29,6 @@ class ChatScreen extends StatefulWidget {
required this.peerPubkey, required this.peerPubkey,
this.messageStore, this.messageStore,
this.profileCache, this.profileCache,
this.trustReferents,
this.wotSettings,
super.key, super.key,
}); });
@ -49,10 +45,6 @@ class ChatScreen extends StatefulWidget {
/// Optional cache of peer display names; null in tests. /// Optional cache of peer display names; null in tests.
final ProfileCache? profileCache; final ProfileCache? profileCache;
/// Web-of-trust bootstrap referents + parameters, for the membership verdict.
final TrustReferents? trustReferents;
final WotSettings? wotSettings;
@override @override
State<ChatScreen> createState() => _ChatScreenState(); State<ChatScreen> createState() => _ChatScreenState();
} }
@ -88,8 +80,6 @@ class _ChatScreenState extends State<ChatScreen> {
// connected / unreachable) null transports and the screen degrades. // connected / unreachable) null transports and the screen degrades.
final session = await widget.connection.session(); final session = await widget.connection.session();
final cachedName = await widget.profileCache?.name(widget.peerPubkey); final cachedName = await widget.profileCache?.name(widget.peerPubkey);
final referents = await widget.trustReferents?.all() ?? const <String>{};
final wotParams = await widget.wotSettings?.params() ?? WotParams.duniter;
if (!mounted) return; // shared session is owned by the connection, not us if (!mounted) return; // shared session is owned by the connection, not us
final self = widget.social.publicKeyHex; final self = widget.social.publicKeyHex;
final messages = MessagesCubit( final messages = MessagesCubit(
@ -102,8 +92,6 @@ class _ChatScreenState extends State<ChatScreen> {
session?.trust, session?.trust,
peerPubkey: widget.peerPubkey, peerPubkey: widget.peerPubkey,
selfPubkey: self, selfPubkey: self,
referents: referents,
params: wotParams,
); );
unawaited(trust.load()); unawaited(trust.load());
setState(() { setState(() {
@ -323,10 +311,9 @@ class _TrustBanner extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
final cubit = context.read<TrustCubit>(); final cubit = context.read<TrustCubit>();
if (!cubit.isOnline || state.loading) return const SizedBox.shrink(); if (!cubit.isOnline || state.loading) return const SizedBox.shrink();
// Strongest applicable signal decides the badge (member > circle > // Strongest applicable signal decides the badge (circle > vouched >
// vouched > unknown). // unknown).
final (IconData icon, String label, bool strong) = switch (state.tier) { 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.inYourCircle => (Icons.verified_user, t.trust.circle, true),
TrustTier.vouched => ( TrustTier.vouched => (
Icons.people_outline, Icons.people_outline,

View file

@ -22,7 +22,7 @@ class ProfileScreen extends StatefulWidget {
required this.connection, required this.connection,
required this.profileStore, required this.profileStore,
required this.accounts, required this.accounts,
this.trustNetworkEnabled = false, this.yourPeopleEnabled = false,
super.key, super.key,
}); });
@ -33,8 +33,8 @@ class ProfileScreen extends StatefulWidget {
final ProfileStore profileStore; final ProfileStore profileStore;
final SocialAccountStore accounts; final SocialAccountStore accounts;
/// Whether to offer the "Network of trust" entry (needs the WoT services). /// Whether to offer the "Your people" entry (needs the social layer).
final bool trustNetworkEnabled; final bool yourPeopleEnabled;
@override @override
State<ProfileScreen> createState() => _ProfileScreenState(); State<ProfileScreen> createState() => _ProfileScreenState();
@ -210,16 +210,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
onSwitch: (a) => _switchTo(a), onSwitch: (a) => _switchTo(a),
onNew: _newIdentity, onNew: _newIdentity,
), ),
if (widget.trustNetworkEnabled) ...[ if (widget.yourPeopleEnabled) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
const Divider(), const Divider(),
ListTile( ListTile(
key: const Key('profile.trustNetwork'), key: const Key('profile.yourPeople'),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.hub_outlined, color: seedGreen), leading:
title: Text(t.wot.open), const Icon(Icons.people_alt_outlined, color: seedGreen),
title: Text(t.yourPeople.title),
trailing: const Icon(Icons.chevron_right, color: seedMuted), trailing: const Icon(Icons.chevron_right, color: seedMuted),
onTap: () => context.push('/trust'), onTap: () => context.push('/your-people'),
), ),
], ],
], ],

View file

@ -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<TrustNetworkScreen> createState() => _TrustNetworkScreenState();
}
class _TrustNetworkScreenState extends State<TrustNetworkScreen> {
final _input = TextEditingController();
Set<String> _roots = {};
WotParams _params = WotParams.duniter;
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_load();
}
Future<void> _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<void> _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<void> _removeRoot(String hex) async {
await widget.referents.remove(hex);
await _load();
}
Future<void> _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<int> 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),
),
],
),
);
}
}

View file

@ -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<YourPeopleScreen> createState() => _YourPeopleScreenState();
}
class _YourPeopleScreenState extends State<YourPeopleScreen> {
TrustTransport? _transport;
bool _loading = true;
bool _busy = false;
List<String> _youVouchFor = const [];
List<String> _vouchForYou = const [];
final Map<String, String> _names = {};
@override
void initState() {
super.initState();
_init();
}
Future<void> _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<void> _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<void> _revoke(String peer) async {
final t = context.t;
final confirmed = await showDialog<bool>(
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,
);
}
}

View file

@ -130,9 +130,6 @@ flutter:
# Seed strings for slang live under lib/i18n (not a Flutter asset; compiled). # Seed strings for slang live under lib/i18n (not a Flutter asset; compiled).
assets: assets:
- assets/catalog/species.json - 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 - assets/logo.png
# Bundled so the Linux runner can use it as the GTK window icon. # Bundled so the Linux runner can use it as the GTK window icon.
- assets/icon.png - assets/icon.png

View file

@ -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);
});
}

View file

@ -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);
});
}

View file

@ -83,6 +83,7 @@ void main() {
final inCircle = TrustCubit(transport, peerPubkey: peer, selfPubkey: me); final inCircle = TrustCubit(transport, peerPubkey: peer, selfPubkey: me);
await inCircle.load(); await inCircle.load();
expect(inCircle.state.knownToYou, isTrue); expect(inCircle.state.knownToYou, isTrue);
expect(inCircle.state.tier, TrustTier.inYourCircle);
await inCircle.close(); await inCircle.close();
final outside = TrustCubit(transport, peerPubkey: 'other', selfPubkey: me); final outside = TrustCubit(transport, peerPubkey: 'other', selfPubkey: me);
@ -91,39 +92,6 @@ void main() {
await outside.close(); 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 { test('tier falls back to vouched, then unknown', () async {
final vouched = TrustCubit( final vouched = TrustCubit(
FakeTrustTransport(me)..addCert('someone', peer), FakeTrustTransport(me)..addCert('someone', peer),

View file

@ -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<Certification> certs = [];
void addCert(String issuer, String subject) => certs.add(Certification(
issuer: issuer,
subject: subject,
issuedAt: DateTime(2026),
));
@override
Future<List<Certification>> allCertifications() async => List.of(certs);
@override
Future<Set<String>> certifiersOf(String subjectPubkey) async => {
for (final c in certs)
if (c.subject == subjectPubkey) c.issuer,
};
@override
Future<void> certify({
required String subjectPubkey,
Duration validity = const Duration(days: 365),
String note = '',
}) async =>
addCert(selfId, subjectPubkey);
@override
Future<void> revoke({required String subjectPubkey}) async => certs
.removeWhere((c) => c.issuer == selfId && c.subject == subjectPubkey);
@override
Future<void> 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<void> 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,
);
});
}