Merge branch 'claude/festive-einstein-384c45'
Ego-centric trust replaces the global Duniter WoT, plus Wallapop-style ratings v1 (kind 30778). Conflicts resolved: pubspec assets (kept seed_saving, dropped trust referents), chat_screen (avatars/no-links from main + rating strip and simplified TrustCubit from the branch), strings.g.dart regenerated from merged sources.
This commit is contained in:
commit
15511ee761
40 changed files with 1777 additions and 876 deletions
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
@ -184,6 +172,7 @@ class TaneApp extends StatelessWidget {
|
|||
connection: connection,
|
||||
mine: offer.authorPubkeyHex == social.publicKeyHex,
|
||||
profileCache: profileCache,
|
||||
selfPubkey: social.publicKeyHex,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -208,16 +197,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 +218,6 @@ class TaneApp extends StatelessWidget {
|
|||
peerPubkey: state.pathParameters['pubkey']!,
|
||||
messageStore: messageStore,
|
||||
profileCache: profileCache,
|
||||
trustReferents: trustReferents,
|
||||
wotSettings: wotSettings,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
|
|
|
|||
|
|
@ -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<Bootstrap> {
|
|||
profileStore: getIt<ProfileStore>(),
|
||||
profileCache: getIt<ProfileCache>(),
|
||||
socialAccounts: getIt<SocialAccountStore>(),
|
||||
trustReferents: getIt<TrustReferents>(),
|
||||
wotSettings: getIt<WotSettings>(),
|
||||
inbox: inbox,
|
||||
notifications: notifications,
|
||||
showIntro: !await onboarding.introSeen(),
|
||||
|
|
|
|||
|
|
@ -44,9 +44,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
|
||||
|
|
@ -175,9 +173,6 @@ Future<void> configureDependencies() async {
|
|||
..registerSingleton<LocaleStore>(LocaleStore(secretStore))
|
||||
..registerSingleton<SocialSettings>(SocialSettings(secretStore))
|
||||
..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))
|
||||
// Per-identity stores are namespaced by the active account's scope.
|
||||
..registerSingleton<MessageStore>(
|
||||
|
|
|
|||
|
|
@ -568,29 +568,26 @@
|
|||
"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"
|
||||
},
|
||||
"ratings": {
|
||||
"rate": "Valorar a esta persona",
|
||||
"edit": "Editar la to valoración",
|
||||
"commentHint": "¿Qué tal foi? (opcional)",
|
||||
"fromYourCircle": "{n} de xente que conoces",
|
||||
"retract": "Quitar la to valoración",
|
||||
"saved": "Valoración guardada"
|
||||
},
|
||||
"notifications": {
|
||||
"newMessageFrom": "Mensaxe nuevu de {name}"
|
||||
|
|
|
|||
|
|
@ -571,29 +571,26 @@
|
|||
"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"
|
||||
},
|
||||
"ratings": {
|
||||
"rate": "Rate this person",
|
||||
"edit": "Edit your rating",
|
||||
"commentHint": "How did it go? (optional)",
|
||||
"fromYourCircle": "{n} from people you know",
|
||||
"retract": "Remove your rating",
|
||||
"saved": "Rating saved"
|
||||
},
|
||||
"notifications": {
|
||||
"newMessageFrom": "New message from {name}"
|
||||
|
|
|
|||
|
|
@ -570,29 +570,26 @@
|
|||
"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"
|
||||
},
|
||||
"ratings": {
|
||||
"rate": "Valorar a esta persona",
|
||||
"edit": "Editar tu valoración",
|
||||
"commentHint": "¿Qué tal fue? (opcional)",
|
||||
"fromYourCircle": "{n} de gente que conoces",
|
||||
"retract": "Quitar tu valoración",
|
||||
"saved": "Valoración guardada"
|
||||
},
|
||||
"notifications": {
|
||||
"newMessageFrom": "Nuevo mensaje de {name}"
|
||||
|
|
|
|||
|
|
@ -567,29 +567,26 @@
|
|||
"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"
|
||||
},
|
||||
"ratings": {
|
||||
"rate": "Avaliar esta pessoa",
|
||||
"edit": "Editar a tua avaliação",
|
||||
"commentHint": "Como correu? (opcional)",
|
||||
"fromYourCircle": "{n} de pessoas que conheces",
|
||||
"retract": "Remover a tua avaliação",
|
||||
"saved": "Avaliação guardada"
|
||||
},
|
||||
"notifications": {
|
||||
"newMessageFrom": "Nova mensagem de {name}"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 4
|
||||
/// Strings: 2020 (505 per locale)
|
||||
/// Strings: 2000 (500 per locale)
|
||||
///
|
||||
/// Built on 2026-07-11 at 06:00 UTC
|
||||
/// Built on 2026-07-11 at 11:22 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ class TranslationsAst extends Translations with BaseTranslations<AppLocale, Tran
|
|||
@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$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$ratings$ast ratings = _Translations$ratings$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$handover$ast handover = _Translations$handover$ast._(_root);
|
||||
|
|
@ -826,35 +827,39 @@ class _Translations$trust$ast extends Translations$trust$en {
|
|||
@override String get vouch => '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: ratings
|
||||
class _Translations$ratings$ast extends Translations$ratings$en {
|
||||
_Translations$ratings$ast._(TranslationsAst root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsAst _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get rate => 'Valorar a esta persona';
|
||||
@override String get edit => 'Editar la to valoración';
|
||||
@override String get commentHint => '¿Qué tal foi? (opcional)';
|
||||
@override String fromYourCircle({required Object n}) => '${n} de xente que conoces';
|
||||
@override String get retract => 'Quitar la to valoración';
|
||||
@override String get saved => 'Valoración guardada';
|
||||
}
|
||||
|
||||
// Path: notifications
|
||||
|
|
@ -1718,26 +1723,21 @@ 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',
|
||||
'ratings.rate' => 'Valorar a esta persona',
|
||||
'ratings.edit' => 'Editar la to valoración',
|
||||
'ratings.commentHint' => '¿Qué tal foi? (opcional)',
|
||||
'ratings.fromYourCircle' => ({required Object n}) => '${n} de xente que conoces',
|
||||
'ratings.retract' => 'Quitar la to valoración',
|
||||
'ratings.saved' => 'Valoración guardada',
|
||||
'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.',
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
|||
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$ratings$en ratings = Translations$ratings$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$handover$en handover = Translations$handover$en.internal(_root);
|
||||
|
|
@ -1565,75 +1566,69 @@ 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: 'You're offline — try again when you're connected'
|
||||
String get offline => 'You\'re offline — try again when you\'re connected';
|
||||
}
|
||||
|
||||
/// en: 'Trust root added'
|
||||
String get rootAdded => 'Trust root added';
|
||||
// Path: ratings
|
||||
class Translations$ratings$en {
|
||||
Translations$ratings$en.internal(this._root);
|
||||
|
||||
/// en: 'That's not a valid identity code'
|
||||
String get rootInvalid => 'That\'s not a valid identity code';
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
/// en: 'Remove'
|
||||
String get remove => 'Remove';
|
||||
// Translations
|
||||
|
||||
/// en: 'Advanced parameters'
|
||||
String get params => 'Advanced parameters';
|
||||
/// en: 'Rate this person'
|
||||
String get rate => 'Rate this person';
|
||||
|
||||
/// 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: 'Edit your rating'
|
||||
String get edit => 'Edit your rating';
|
||||
|
||||
/// en: 'Vouches to become a member'
|
||||
String get sigQty => 'Vouches to become a member';
|
||||
/// en: 'How did it go? (optional)'
|
||||
String get commentHint => 'How did it go? (optional)';
|
||||
|
||||
/// en: 'Max distance from a root'
|
||||
String get stepMax => 'Max distance from a root';
|
||||
/// en: '{n} from people you know'
|
||||
String fromYourCircle({required Object n}) => '${n} from people you know';
|
||||
|
||||
/// en: 'Vouch validity (days)'
|
||||
String get validityDays => 'Vouch validity (days)';
|
||||
/// en: 'Remove your rating'
|
||||
String get retract => 'Remove your rating';
|
||||
|
||||
/// en: 'Reset to Duniter defaults'
|
||||
String get reset => 'Reset to Duniter defaults';
|
||||
|
||||
/// en: 'Saved'
|
||||
String get saved => 'Saved';
|
||||
/// en: 'Rating saved'
|
||||
String get saved => 'Rating saved';
|
||||
}
|
||||
|
||||
// Path: notifications
|
||||
|
|
@ -2718,26 +2713,21 @@ 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',
|
||||
'ratings.rate' => 'Rate this person',
|
||||
'ratings.edit' => 'Edit your rating',
|
||||
'ratings.commentHint' => 'How did it go? (optional)',
|
||||
'ratings.fromYourCircle' => ({required Object n}) => '${n} from people you know',
|
||||
'ratings.retract' => 'Remove your rating',
|
||||
'ratings.saved' => 'Rating saved',
|
||||
'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.',
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
|||
@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$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$ratings$es ratings = _Translations$ratings$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$handover$es handover = _Translations$handover$es._(_root);
|
||||
|
|
@ -828,35 +829,39 @@ class _Translations$trust$es extends Translations$trust$en {
|
|||
@override String get vouch => '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: ratings
|
||||
class _Translations$ratings$es extends Translations$ratings$en {
|
||||
_Translations$ratings$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get rate => 'Valorar a esta persona';
|
||||
@override String get edit => 'Editar tu valoración';
|
||||
@override String get commentHint => '¿Qué tal fue? (opcional)';
|
||||
@override String fromYourCircle({required Object n}) => '${n} de gente que conoces';
|
||||
@override String get retract => 'Quitar tu valoración';
|
||||
@override String get saved => 'Valoración guardada';
|
||||
}
|
||||
|
||||
// Path: notifications
|
||||
|
|
@ -1722,26 +1727,21 @@ 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',
|
||||
'ratings.rate' => 'Valorar a esta persona',
|
||||
'ratings.edit' => 'Editar tu valoración',
|
||||
'ratings.commentHint' => '¿Qué tal fue? (opcional)',
|
||||
'ratings.fromYourCircle' => ({required Object n}) => '${n} de gente que conoces',
|
||||
'ratings.retract' => 'Quitar tu valoración',
|
||||
'ratings.saved' => 'Valoración guardada',
|
||||
'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.',
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ class TranslationsPt extends Translations with BaseTranslations<AppLocale, Trans
|
|||
@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$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$ratings$pt ratings = _Translations$ratings$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$handover$pt handover = _Translations$handover$pt._(_root);
|
||||
|
|
@ -825,35 +826,39 @@ class _Translations$trust$pt extends Translations$trust$en {
|
|||
@override String get vouch => '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: ratings
|
||||
class _Translations$ratings$pt extends Translations$ratings$en {
|
||||
_Translations$ratings$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsPt _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get rate => 'Avaliar esta pessoa';
|
||||
@override String get edit => 'Editar a tua avaliação';
|
||||
@override String get commentHint => 'Como correu? (opcional)';
|
||||
@override String fromYourCircle({required Object n}) => '${n} de pessoas que conheces';
|
||||
@override String get retract => 'Remover a tua avaliação';
|
||||
@override String get saved => 'Avaliação guardada';
|
||||
}
|
||||
|
||||
// Path: notifications
|
||||
|
|
@ -1716,26 +1721,21 @@ 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',
|
||||
'ratings.rate' => 'Avaliar esta pessoa',
|
||||
'ratings.edit' => 'Editar a tua avaliação',
|
||||
'ratings.commentHint' => 'Como correu? (opcional)',
|
||||
'ratings.fromYourCircle' => ({required Object n}) => '${n} de pessoas que conheces',
|
||||
'ratings.retract' => 'Remover a tua avaliação',
|
||||
'ratings.saved' => 'Avaliação guardada',
|
||||
'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.',
|
||||
|
|
|
|||
|
|
@ -89,13 +89,14 @@ class SocialService {
|
|||
}
|
||||
}
|
||||
|
||||
/// One relay channel with the three transports on top — the
|
||||
/// "one channel, three interfaces" shape, at the app layer.
|
||||
/// One relay channel with the transports on top — the
|
||||
/// "one channel, N interfaces" shape, at the app layer.
|
||||
class SocialSession {
|
||||
SocialSession(this._channel, {String deviceId = ''})
|
||||
: offers = NostrOfferTransport(_channel),
|
||||
messages = NostrMessageTransport(_channel),
|
||||
trust = NostrTrustTransport(_channel),
|
||||
ratings = NostrRatingTransport(_channel),
|
||||
profile = NostrProfileTransport(_channel),
|
||||
sync = NostrSyncTransport(
|
||||
_channel,
|
||||
|
|
@ -108,6 +109,7 @@ class SocialSession {
|
|||
final OfferTransport offers;
|
||||
final MessageTransport messages;
|
||||
final TrustTransport trust;
|
||||
final RatingTransport ratings;
|
||||
final ProfileTransport profile;
|
||||
|
||||
/// Device-to-device inventory sync for THIS identity's own devices.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
145
apps/app_seeds/lib/state/peer_rating_cubit.dart
Normal file
145
apps/app_seeds/lib/state/peer_rating_cubit.dart
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Reputation of one peer, seen from this user's position: everyone's active
|
||||
/// ratings plus how many of those come from people in your own circle — the
|
||||
/// signal that makes strangers' review-stuffing irrelevant.
|
||||
class PeerRatingState extends Equatable {
|
||||
const PeerRatingState({
|
||||
this.count = 0,
|
||||
this.average = 0,
|
||||
this.circleCount = 0,
|
||||
this.myStars,
|
||||
this.myComment = '',
|
||||
this.loading = true,
|
||||
this.busy = false,
|
||||
});
|
||||
|
||||
/// Active ratings of the peer, one per rater.
|
||||
final int count;
|
||||
|
||||
/// Mean stars over [count]; 0 when there are none.
|
||||
final double average;
|
||||
|
||||
/// How many of those ratings come from your circle (you or a
|
||||
/// friend-of-a-friend). These are the ones worth highlighting.
|
||||
final int circleCount;
|
||||
|
||||
/// Your own rating of the peer, if any.
|
||||
final int? myStars;
|
||||
final String myComment;
|
||||
|
||||
final bool loading;
|
||||
final bool busy;
|
||||
|
||||
bool get hasRatings => count > 0;
|
||||
bool get iRated => myStars != null;
|
||||
|
||||
PeerRatingState copyWith({
|
||||
int? count,
|
||||
double? average,
|
||||
int? circleCount,
|
||||
int? Function()? myStars,
|
||||
String? myComment,
|
||||
bool? loading,
|
||||
bool? busy,
|
||||
}) =>
|
||||
PeerRatingState(
|
||||
count: count ?? this.count,
|
||||
average: average ?? this.average,
|
||||
circleCount: circleCount ?? this.circleCount,
|
||||
myStars: myStars != null ? myStars() : this.myStars,
|
||||
myComment: myComment ?? this.myComment,
|
||||
loading: loading ?? this.loading,
|
||||
busy: busy ?? this.busy,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[count, average, circleCount, myStars, myComment, loading, busy];
|
||||
}
|
||||
|
||||
/// Reads and edits this user's rating of [peerPubkey] over a
|
||||
/// [RatingTransport], and summarises everyone else's. The circle weighting
|
||||
/// reuses the trust graph (same ego-centric rule as TrustCubit).
|
||||
class PeerRatingCubit extends Cubit<PeerRatingState> {
|
||||
PeerRatingCubit(
|
||||
this._ratings, {
|
||||
required this.peerPubkey,
|
||||
required this.selfPubkey,
|
||||
TrustTransport? trust,
|
||||
}) : _trust = trust,
|
||||
super(const PeerRatingState());
|
||||
|
||||
final RatingTransport? _ratings;
|
||||
final TrustTransport? _trust;
|
||||
final String peerPubkey;
|
||||
final String selfPubkey;
|
||||
|
||||
bool get isOnline => _ratings != null;
|
||||
|
||||
/// Same personal "circle" rule as TrustCubit: one vouch from your side, out
|
||||
/// to a friend-of-a-friend.
|
||||
static const _circleThreshold = 1;
|
||||
static const _circleDistance = 2;
|
||||
|
||||
Future<void> load() async {
|
||||
final ratings = _ratings;
|
||||
if (ratings == null) {
|
||||
emit(state.copyWith(loading: false));
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(loading: true));
|
||||
final all = await ratings.ratingsOf(peerPubkey);
|
||||
final mine = await ratings.myRatingOf(peerPubkey);
|
||||
|
||||
var circle = const <String>{};
|
||||
final trust = _trust;
|
||||
if (trust != null && all.isNotEmpty) {
|
||||
final wot = WebOfTrust.fromCertifications(
|
||||
await trust.allCertifications(),
|
||||
now: DateTime.now(),
|
||||
);
|
||||
circle = wot.members(
|
||||
seeds: {selfPubkey},
|
||||
threshold: _circleThreshold,
|
||||
maxDistance: _circleDistance,
|
||||
);
|
||||
}
|
||||
|
||||
final total = all.fold<int>(0, (sum, r) => sum + r.stars);
|
||||
emit(state.copyWith(
|
||||
count: all.length,
|
||||
average: all.isEmpty ? 0 : total / all.length,
|
||||
circleCount: all.where((r) => circle.contains(r.rater)).length,
|
||||
myStars: () => mine?.stars,
|
||||
myComment: mine?.comment ?? '',
|
||||
loading: false,
|
||||
));
|
||||
}
|
||||
|
||||
/// Publishes (or replaces) your rating, then reloads. Never rates self.
|
||||
Future<void> rate(int stars, {String comment = ''}) async {
|
||||
final ratings = _ratings;
|
||||
if (ratings == null || peerPubkey == selfPubkey) return;
|
||||
emit(state.copyWith(busy: true));
|
||||
await ratings.rate(
|
||||
subjectPubkey: peerPubkey,
|
||||
stars: stars,
|
||||
comment: comment,
|
||||
);
|
||||
await load();
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
|
||||
/// Takes your rating back, then reloads.
|
||||
Future<void> retract() async {
|
||||
final ratings = _ratings;
|
||||
if (ratings == null) return;
|
||||
emit(state.copyWith(busy: true));
|
||||
await ratings.retract(subjectPubkey: peerPubkey);
|
||||
await load();
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TrustState> {
|
||||
TrustCubit(
|
||||
this._transport, {
|
||||
required this.peerPubkey,
|
||||
required this.selfPubkey,
|
||||
this.referents = const {},
|
||||
this.params = WotParams.duniter,
|
||||
Future<void> Function()? onDispose,
|
||||
}) : _onDispose = onDispose,
|
||||
super(const TrustState());
|
||||
|
|
@ -110,26 +88,20 @@ class TrustCubit extends Cubit<TrustState> {
|
|||
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<String> referents;
|
||||
|
||||
/// Active web-of-trust parameters (sigQty / stepMax / validity).
|
||||
final WotParams params;
|
||||
final Future<void> 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<void> load() async {
|
||||
final transport = _transport;
|
||||
if (transport == null) {
|
||||
|
|
@ -147,23 +119,17 @@ class TrustCubit extends Cubit<TrustState> {
|
|||
threshold: _circleThreshold,
|
||||
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(
|
||||
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<void> toggleVouch() async {
|
||||
final transport = _transport;
|
||||
if (transport == null || peerPubkey == selfPubkey) return;
|
||||
|
|
@ -173,7 +139,7 @@ class TrustCubit extends Cubit<TrustState> {
|
|||
} else {
|
||||
await transport.certify(
|
||||
subjectPubkey: peerPubkey,
|
||||
validity: params.sigValidity,
|
||||
validity: _vouchValidity,
|
||||
);
|
||||
}
|
||||
await load();
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ import '../services/profile_cache.dart';
|
|||
import '../services/profile_store.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/peer_rating_cubit.dart';
|
||||
import '../state/trust_cubit.dart';
|
||||
import 'peer_avatar.dart';
|
||||
import 'rating_sheet.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// A 1:1 encrypted chat with one peer (redesign screen 10). Private and
|
||||
|
|
@ -33,8 +33,6 @@ class ChatScreen extends StatefulWidget {
|
|||
required this.peerPubkey,
|
||||
this.messageStore,
|
||||
this.profileCache,
|
||||
this.trustReferents,
|
||||
this.wotSettings,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -51,10 +49,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<ChatScreen> createState() => _ChatScreenState();
|
||||
}
|
||||
|
|
@ -62,6 +56,7 @@ class ChatScreen extends StatefulWidget {
|
|||
class _ChatScreenState extends State<ChatScreen> {
|
||||
MessagesCubit? _messages;
|
||||
TrustCubit? _trust;
|
||||
PeerRatingCubit? _rating;
|
||||
bool _loading = true;
|
||||
String? _peerName;
|
||||
String? _selfName;
|
||||
|
|
@ -95,8 +90,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
final selfName = getIt.isRegistered<ProfileStore>()
|
||||
? await getIt<ProfileStore>().name()
|
||||
: '';
|
||||
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
|
||||
final self = widget.social.publicKeyHex;
|
||||
final messages = MessagesCubit(
|
||||
|
|
@ -109,13 +102,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
session?.trust,
|
||||
peerPubkey: widget.peerPubkey,
|
||||
selfPubkey: self,
|
||||
referents: referents,
|
||||
params: wotParams,
|
||||
);
|
||||
unawaited(trust.load());
|
||||
final rating = PeerRatingCubit(
|
||||
session?.ratings,
|
||||
peerPubkey: widget.peerPubkey,
|
||||
selfPubkey: self,
|
||||
trust: session?.trust,
|
||||
);
|
||||
unawaited(rating.load());
|
||||
setState(() {
|
||||
_messages = messages;
|
||||
_trust = trust;
|
||||
_rating = rating;
|
||||
_peerName = cachedName;
|
||||
_selfName = selfName.isEmpty ? null : selfName;
|
||||
_loading = false;
|
||||
|
|
@ -193,6 +192,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
unawaited(_unread?.markRead(widget.peerPubkey));
|
||||
_messages?.close();
|
||||
_trust?.close();
|
||||
_rating?.close();
|
||||
// The session belongs to the shared connection — don't close it here.
|
||||
_input.dispose();
|
||||
super.dispose();
|
||||
|
|
@ -202,6 +202,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
final messages = _messages;
|
||||
final trust = _trust;
|
||||
final rating = _rating;
|
||||
final t = context.t;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -216,12 +217,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
body: _loading || messages == null || trust == null
|
||||
body: _loading || messages == null || trust == null || rating == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: messages),
|
||||
BlocProvider.value(value: trust),
|
||||
BlocProvider.value(value: rating),
|
||||
],
|
||||
child: _ChatBody(
|
||||
controller: _input,
|
||||
|
|
@ -274,6 +276,7 @@ class _ChatBody extends StatelessWidget {
|
|||
child: Column(
|
||||
children: [
|
||||
const _TrustBanner(),
|
||||
const _RatingStrip(),
|
||||
Expanded(
|
||||
child: ChatMessageList(peerName: peerName, selfName: selfName),
|
||||
),
|
||||
|
|
@ -348,10 +351,9 @@ class _TrustBanner extends StatelessWidget {
|
|||
builder: (context, state) {
|
||||
final cubit = context.read<TrustCubit>();
|
||||
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,
|
||||
|
|
@ -391,6 +393,56 @@ class _TrustBanner extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// A slim reputation strip under the trust banner: everyone's stars for this
|
||||
/// peer (highlighting how many come from people you know) and, once you've
|
||||
/// actually talked with them, a button to leave or edit your own rating.
|
||||
class _RatingStrip extends StatelessWidget {
|
||||
const _RatingStrip();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
return BlocBuilder<PeerRatingCubit, PeerRatingState>(
|
||||
builder: (context, state) {
|
||||
final cubit = context.read<PeerRatingCubit>();
|
||||
if (!cubit.isOnline || state.loading) return const SizedBox.shrink();
|
||||
// Soft anchor: you can only rate someone you've talked with.
|
||||
final canRate = context.select<MessagesCubit, bool>(
|
||||
(m) => m.state.messages.isNotEmpty,
|
||||
);
|
||||
if (!state.hasRatings && !canRate) return const SizedBox.shrink();
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: seedPrimaryContainer.withValues(alpha: 0.3),
|
||||
padding: const EdgeInsetsDirectional.fromSTEB(16, 4, 8, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: state.hasRatings
|
||||
? RatingStars(
|
||||
average: state.average,
|
||||
count: state.count,
|
||||
circleCount: state.circleCount,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
if (canRate)
|
||||
TextButton(
|
||||
key: const Key('chat.rate'),
|
||||
onPressed: state.busy
|
||||
? null
|
||||
: () => showRatingSheet(context, cubit),
|
||||
child:
|
||||
Text(state.iRated ? t.ratings.edit : t.ratings.rate),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A centered pill introducing a new day ("Today", "Yesterday", or a
|
||||
/// locale-formatted date). Localized via [chatDayLabel].
|
||||
class _DaySeparator extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ 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 '../state/peer_rating_cubit.dart';
|
||||
import 'market_widgets.dart';
|
||||
import 'rating_sheet.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// Read-only "product page" for one discovered [Offer] (the Wallapop-style
|
||||
|
|
@ -22,6 +25,7 @@ class MarketOfferDetailScreen extends StatefulWidget {
|
|||
required this.connection,
|
||||
this.mine = false,
|
||||
this.profileCache,
|
||||
this.selfPubkey,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -36,6 +40,10 @@ class MarketOfferDetailScreen extends StatefulWidget {
|
|||
/// Optional cache of peers' published display names; null in tests.
|
||||
final ProfileCache? profileCache;
|
||||
|
||||
/// This user's key (hex), to weigh the author's ratings by your own circle;
|
||||
/// null in tests (circle weighting then stays off).
|
||||
final String? selfPubkey;
|
||||
|
||||
@override
|
||||
State<MarketOfferDetailScreen> createState() =>
|
||||
_MarketOfferDetailScreenState();
|
||||
|
|
@ -46,6 +54,7 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
|
|||
String? _sellerAbout;
|
||||
String? _sellerG1;
|
||||
bool _profileLoading = true;
|
||||
PeerRatingState? _sellerRating;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -65,6 +74,7 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
|
|||
setState(() => _profileLoading = false);
|
||||
return;
|
||||
}
|
||||
unawaited(_loadSellerRating(session));
|
||||
try {
|
||||
final profile = await session.profile.fetch(widget.offer.authorPubkeyHex);
|
||||
if (profile != null && profile.name.isNotEmpty) {
|
||||
|
|
@ -86,6 +96,25 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Best-effort summary of the author's ratings, circle-weighted when we know
|
||||
/// who this user is. Reuses the cubit's aggregation without providing it.
|
||||
Future<void> _loadSellerRating(SocialSession session) async {
|
||||
final cubit = PeerRatingCubit(
|
||||
session.ratings,
|
||||
peerPubkey: widget.offer.authorPubkeyHex,
|
||||
selfPubkey: widget.selfPubkey ?? '',
|
||||
trust: widget.selfPubkey == null ? null : session.trust,
|
||||
);
|
||||
try {
|
||||
await cubit.load();
|
||||
if (mounted) setState(() => _sellerRating = cubit.state);
|
||||
} catch (_) {
|
||||
// offline / unreachable — the seller section simply shows no stars.
|
||||
} finally {
|
||||
await cubit.close();
|
||||
}
|
||||
}
|
||||
|
||||
// No dispose of the session — it belongs to the shared connection.
|
||||
|
||||
Future<void> _copyId() async {
|
||||
|
|
@ -167,6 +196,7 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
|
|||
title: t.market.sharedBy,
|
||||
name: _sellerName ?? shortPubkey(o.authorPubkeyHex),
|
||||
about: _sellerAbout,
|
||||
rating: _sellerRating,
|
||||
loading: _profileLoading,
|
||||
hasProfile:
|
||||
_sellerName != null || _sellerAbout != null || _sellerG1 != null,
|
||||
|
|
@ -227,11 +257,16 @@ class _SellerSection extends StatelessWidget {
|
|||
required this.noProfileLabel,
|
||||
required this.onCopyId,
|
||||
required this.copyIdTooltip,
|
||||
this.rating,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String name;
|
||||
final String? about;
|
||||
|
||||
/// Circle-weighted summary of the author's ratings; stars show only when
|
||||
/// someone actually rated them (absence is never a reproach).
|
||||
final PeerRatingState? rating;
|
||||
final bool loading;
|
||||
final bool hasProfile;
|
||||
final String noProfileLabel;
|
||||
|
|
@ -261,13 +296,26 @@ class _SellerSection extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: seedOnSurface,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: seedOnSurface,
|
||||
),
|
||||
),
|
||||
if (rating != null && rating!.hasRatings) ...[
|
||||
const SizedBox(height: 2),
|
||||
RatingStars(
|
||||
average: rating!.average,
|
||||
count: rating!.count,
|
||||
circleCount: rating!.circleCount,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
|
|
|
|||
|
|
@ -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<ProfileScreen> createState() => _ProfileScreenState();
|
||||
|
|
@ -210,16 +210,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
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'),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
|
|
|||
180
apps/app_seeds/lib/ui/rating_sheet.dart
Normal file
180
apps/app_seeds/lib/ui/rating_sheet.dart
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../state/peer_rating_cubit.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// A compact "★ 4,5 (12)" pill, plus how many ratings come from people you
|
||||
/// know — the signal that makes strangers' reviews easy to weigh.
|
||||
class RatingStars extends StatelessWidget {
|
||||
const RatingStars({
|
||||
required this.average,
|
||||
required this.count,
|
||||
this.circleCount = 0,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double average;
|
||||
final int count;
|
||||
final int circleCount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final avg = NumberFormat('0.#', locale).format(average);
|
||||
return Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 6,
|
||||
children: [
|
||||
const Icon(Icons.star_rounded, size: 18, color: seedGreen),
|
||||
Text(
|
||||
'$avg ($count)',
|
||||
style: const TextStyle(
|
||||
color: seedOnSurface,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (circleCount > 0)
|
||||
Text(
|
||||
t.ratings.fromYourCircle(n: circleCount),
|
||||
style: const TextStyle(color: seedGreen, fontSize: 13),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the bottom sheet to leave, edit or take back your rating of the
|
||||
/// peer behind [cubit]. Pre-filled when you already rated.
|
||||
Future<void> showRatingSheet(BuildContext context, PeerRatingCubit cubit) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: _RatingForm(cubit: cubit),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _RatingForm extends StatefulWidget {
|
||||
const _RatingForm({required this.cubit});
|
||||
|
||||
final PeerRatingCubit cubit;
|
||||
|
||||
@override
|
||||
State<_RatingForm> createState() => _RatingFormState();
|
||||
}
|
||||
|
||||
class _RatingFormState extends State<_RatingForm> {
|
||||
late int _stars;
|
||||
late final TextEditingController _comment;
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final state = widget.cubit.state;
|
||||
_stars = state.myStars ?? 0;
|
||||
_comment = TextEditingController(text: state.myComment);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_comment.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final navigator = Navigator.of(context);
|
||||
final saved = context.t.ratings.saved;
|
||||
setState(() => _saving = true);
|
||||
await widget.cubit.rate(_stars, comment: _comment.text.trim());
|
||||
navigator.pop();
|
||||
messenger.showSnackBar(SnackBar(content: Text(saved)));
|
||||
}
|
||||
|
||||
Future<void> _retract() async {
|
||||
final navigator = Navigator.of(context);
|
||||
setState(() => _saving = true);
|
||||
await widget.cubit.retract();
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
final iRated = widget.cubit.state.iRated;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 20, 24, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
iRated ? t.ratings.edit : t.ratings.rate,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: seedTitle,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
for (var s = 1; s <= 5; s++)
|
||||
IconButton(
|
||||
key: Key('rating.star.$s'),
|
||||
iconSize: 36,
|
||||
onPressed:
|
||||
_saving ? null : () => setState(() => _stars = s),
|
||||
icon: Icon(
|
||||
s <= _stars
|
||||
? Icons.star_rounded
|
||||
: Icons.star_outline_rounded,
|
||||
color: s <= _stars ? seedGreen : seedMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
key: const Key('rating.comment'),
|
||||
controller: _comment,
|
||||
enabled: !_saving,
|
||||
maxLength: 280,
|
||||
maxLines: 2,
|
||||
decoration: InputDecoration(
|
||||
hintText: t.ratings.commentHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
if (iRated)
|
||||
TextButton(
|
||||
key: const Key('rating.retract'),
|
||||
onPressed: _saving ? null : _retract,
|
||||
child: Text(t.ratings.retract),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
key: const Key('rating.save'),
|
||||
onPressed: _saving || _stars == 0 ? null : _save,
|
||||
child: Text(t.common.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
240
apps/app_seeds/lib/ui/your_people_screen.dart
Normal file
240
apps/app_seeds/lib/ui/your_people_screen.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -132,9 +132,6 @@ flutter:
|
|||
- assets/catalog/species.json
|
||||
# Curated seed-saving guidance (pollination, isolation, difficulty…).
|
||||
- assets/catalog/seed_saving.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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
162
apps/app_seeds/test/state/peer_rating_cubit_test.dart
Normal file
162
apps/app_seeds/test/state/peer_rating_cubit_test.dart
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/state/peer_rating_cubit.dart';
|
||||
|
||||
/// In-memory [RatingTransport]: rate/retract act as this device's identity,
|
||||
/// one live rating per (rater, subject) like the Nostr transport.
|
||||
class FakeRatingTransport implements RatingTransport {
|
||||
FakeRatingTransport(this.selfId);
|
||||
final String selfId;
|
||||
final Map<String, Map<String, Rating>> _bySubject = {}; // subject -> rater
|
||||
|
||||
/// Test helper: record an arbitrary rater→subject rating.
|
||||
void addRating(String rater, String subject, int stars,
|
||||
{String comment = ''}) =>
|
||||
(_bySubject[subject] ??= {})[rater] = Rating(
|
||||
rater: rater,
|
||||
subject: subject,
|
||||
stars: stars,
|
||||
ratedAt: DateTime(2026),
|
||||
comment: comment,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> rate({
|
||||
required String subjectPubkey,
|
||||
required int stars,
|
||||
String comment = '',
|
||||
}) async =>
|
||||
addRating(selfId, subjectPubkey, stars, comment: comment);
|
||||
|
||||
@override
|
||||
Future<void> retract({required String subjectPubkey}) async =>
|
||||
_bySubject[subjectPubkey]?.remove(selfId);
|
||||
|
||||
@override
|
||||
Future<List<Rating>> ratingsOf(String subjectPubkey) async =>
|
||||
List.of(_bySubject[subjectPubkey]?.values ?? const <Rating>[]);
|
||||
|
||||
@override
|
||||
Future<Rating?> myRatingOf(String subjectPubkey) async =>
|
||||
_bySubject[subjectPubkey]?[selfId];
|
||||
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
/// Minimal trust transport exposing a fixed certification graph, for the
|
||||
/// circle weighting.
|
||||
class FakeTrustTransport implements TrustTransport {
|
||||
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 {}
|
||||
|
||||
@override
|
||||
Future<void> revoke({required String subjectPubkey}) async {}
|
||||
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
const me = 'me';
|
||||
const peer = 'peer';
|
||||
|
||||
test('aggregates count and average over active ratings', () async {
|
||||
final ratings = FakeRatingTransport(me)
|
||||
..addRating('a', peer, 5)
|
||||
..addRating('b', peer, 2);
|
||||
final cubit = PeerRatingCubit(ratings, peerPubkey: peer, selfPubkey: me);
|
||||
await cubit.load();
|
||||
expect(cubit.state.count, 2);
|
||||
expect(cubit.state.average, closeTo(3.5, 0.001));
|
||||
expect(cubit.state.circleCount, 0); // no trust graph wired
|
||||
expect(cubit.state.iRated, isFalse);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('counts ratings coming from your circle', () async {
|
||||
final ratings = FakeRatingTransport(me)
|
||||
..addRating('friend', peer, 5) // you vouch for 'friend'
|
||||
..addRating('stranger', peer, 1);
|
||||
final trust = FakeTrustTransport()..addCert(me, 'friend');
|
||||
final cubit = PeerRatingCubit(
|
||||
ratings,
|
||||
peerPubkey: peer,
|
||||
selfPubkey: me,
|
||||
trust: trust,
|
||||
);
|
||||
await cubit.load();
|
||||
expect(cubit.state.count, 2);
|
||||
expect(cubit.state.circleCount, 1);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('rate publishes then reloads; re-rating replaces', () async {
|
||||
final ratings = FakeRatingTransport(me);
|
||||
final cubit = PeerRatingCubit(ratings, peerPubkey: peer, selfPubkey: me);
|
||||
await cubit.load();
|
||||
|
||||
await cubit.rate(4, comment: 'Nice seeds');
|
||||
expect(cubit.state.myStars, 4);
|
||||
expect(cubit.state.myComment, 'Nice seeds');
|
||||
expect(cubit.state.count, 1);
|
||||
|
||||
await cubit.rate(2);
|
||||
expect(cubit.state.myStars, 2);
|
||||
expect(cubit.state.count, 1); // replaced, not piled up
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('retract removes your rating', () async {
|
||||
final ratings = FakeRatingTransport(me)..addRating(me, peer, 3);
|
||||
final cubit = PeerRatingCubit(ratings, peerPubkey: peer, selfPubkey: me);
|
||||
await cubit.load();
|
||||
expect(cubit.state.iRated, isTrue);
|
||||
|
||||
await cubit.retract();
|
||||
expect(cubit.state.iRated, isFalse);
|
||||
expect(cubit.state.count, 0);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('never rates self', () async {
|
||||
final ratings = FakeRatingTransport(me);
|
||||
final cubit = PeerRatingCubit(ratings, peerPubkey: me, selfPubkey: me);
|
||||
await cubit.load();
|
||||
await cubit.rate(5);
|
||||
expect(cubit.state.iRated, isFalse);
|
||||
expect(cubit.state.count, 0);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('offline (no transport) never throws', () async {
|
||||
final cubit = PeerRatingCubit(null, peerPubkey: peer, selfPubkey: me);
|
||||
expect(cubit.isOnline, isFalse);
|
||||
await cubit.load();
|
||||
await cubit.rate(5);
|
||||
await cubit.retract();
|
||||
expect(cubit.state.loading, isFalse);
|
||||
await cubit.close();
|
||||
});
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
124
apps/app_seeds/test/ui/rating_sheet_test.dart
Normal file
124
apps/app_seeds/test/ui/rating_sheet_test.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
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/state/peer_rating_cubit.dart';
|
||||
import 'package:tane/ui/rating_sheet.dart';
|
||||
|
||||
/// In-memory [RatingTransport] (one live rating per pair).
|
||||
class FakeRatingTransport implements RatingTransport {
|
||||
FakeRatingTransport(this.selfId);
|
||||
final String selfId;
|
||||
final Map<String, Rating> byRater = {};
|
||||
|
||||
@override
|
||||
Future<void> rate({
|
||||
required String subjectPubkey,
|
||||
required int stars,
|
||||
String comment = '',
|
||||
}) async =>
|
||||
byRater[selfId] = Rating(
|
||||
rater: selfId,
|
||||
subject: subjectPubkey,
|
||||
stars: stars,
|
||||
ratedAt: DateTime(2026),
|
||||
comment: comment,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> retract({required String subjectPubkey}) async =>
|
||||
byRater.remove(selfId);
|
||||
|
||||
@override
|
||||
Future<List<Rating>> ratingsOf(String subjectPubkey) async =>
|
||||
List.of(byRater.values);
|
||||
|
||||
@override
|
||||
Future<Rating?> myRatingOf(String subjectPubkey) async => byRater[selfId];
|
||||
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
const me = 'me';
|
||||
const peer = 'peer';
|
||||
|
||||
late FakeRatingTransport transport;
|
||||
late PeerRatingCubit cubit;
|
||||
|
||||
setUp(() async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
transport = FakeRatingTransport(me);
|
||||
cubit = PeerRatingCubit(transport, peerPubkey: peer, selfPubkey: me);
|
||||
await cubit.load();
|
||||
});
|
||||
|
||||
tearDown(() => cubit.close());
|
||||
|
||||
Widget host() => TranslationProvider(
|
||||
child: MaterialApp(
|
||||
locale: AppLocale.en.flutterLocale,
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => Center(
|
||||
child: TextButton(
|
||||
onPressed: () => showRatingSheet(context, cubit),
|
||||
child: const Text('open'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('picking stars and saving publishes the rating',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(host());
|
||||
await tester.tap(find.text('open'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Rate this person'), findsOneWidget);
|
||||
// Save is disabled until stars are picked.
|
||||
expect(
|
||||
tester.widget<FilledButton>(find.byKey(const Key('rating.save'))).enabled,
|
||||
isFalse,
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const Key('rating.star.4')));
|
||||
await tester.pump();
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('rating.comment')), 'Great swap');
|
||||
await tester.tap(find.byKey(const Key('rating.save')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final mine = transport.byRater[me]!;
|
||||
expect(mine.stars, 4);
|
||||
expect(mine.comment, 'Great swap');
|
||||
expect(find.text('Rating saved'), findsOneWidget); // snackbar
|
||||
});
|
||||
|
||||
testWidgets('opens pre-filled when you already rated; retract removes',
|
||||
(tester) async {
|
||||
await cubit.rate(3, comment: 'Fine');
|
||||
|
||||
await tester.pumpWidget(host());
|
||||
await tester.tap(find.text('open'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Edit your rating'), findsOneWidget);
|
||||
expect(find.text('Fine'), findsOneWidget); // comment pre-filled
|
||||
|
||||
await tester.tap(find.byKey(const Key('rating.retract')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(transport.byRater, isEmpty);
|
||||
});
|
||||
}
|
||||
176
apps/app_seeds/test/ui/your_people_screen_test.dart
Normal file
176
apps/app_seeds/test/ui/your_people_screen_test.dart
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
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
|
||||
RatingTransport get ratings => 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,
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue