tane/apps/app_seeds/lib/state/trust_cubit.dart
vjrj fef174e8c4 refactor(trust): ego-centric trust replaces the global Duniter membership
The global membership rule (curated bootstrap referents + sigQty/stepMax
parameters) solved sybil-proof identity for a UBI — a problem this app
doesn't have — and its screen leaked graph jargon (npub roots, parameter
steppers). Trust is now computed from the user's own position only:
you vouch / vouched by people you know (distance <=2) / vouched by N.

- TrustCubit: drop networkMember tier, referents and params; keep the
  circle rule and the 365-day vouch expiry (renewable, self-pruning).
- Delete TrustReferents, WotSettings, TrustNetworkScreen and the bundled
  referents asset; unwire injector/bootstrap/app/chat.
- New 'Your people' screen (/your-people, from the profile): who you
  vouch for (revocable) and who vouches for you, names via ProfileCache.
- i18n: wot.* removed, yourPeople.* added (en/es/pt/ast); trust.member
  removed. Kind 30777 events on relays stay fully compatible — only the
  interpretation changes.
2026-07-11 13:03:20 +02:00

154 lines
4.5 KiB
Dart

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. Trust is
/// ego-centric: computed from YOUR position in the public vouch graph,
/// never from a global membership verdict.
enum TrustTier {
/// In your personal circle (you vouch, or a friend-of-a-friend does).
inYourCircle,
/// Vouched for by someone, but not (yet) in your circle.
vouched,
/// No certifications seen.
unknown,
}
/// 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.loading = true,
this.busy = false,
});
/// How many people (network-wide) currently vouch for the peer.
final int certifierCount;
/// Whether this user vouches for the peer.
final bool iVouch;
/// Whether the peer is within the user's own circle (you, or a
/// friend-of-a-friend, vouch). Spam-resistant and works from day one.
final bool knownToYou;
final bool loading;
final bool busy;
/// The strongest applicable signal, for the badge.
TrustTier get tier {
if (knownToYou) return TrustTier.inYourCircle;
if (certifierCount > 0) return TrustTier.vouched;
return TrustTier.unknown;
}
TrustState copyWith({
int? certifierCount,
bool? iVouch,
bool? knownToYou,
bool? loading,
bool? busy,
}) =>
TrustState(
certifierCount: certifierCount ?? this.certifierCount,
iVouch: iVouch ?? this.iVouch,
knownToYou: knownToYou ?? this.knownToYou,
loading: loading ?? this.loading,
busy: busy ?? this.busy,
);
@override
List<Object?> get props => [
certifierCount,
iVouch,
knownToYou,
loading,
busy,
];
}
/// 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,
Future<void> Function()? onDispose,
}) : _onDispose = onDispose,
super(const TrustState());
final TrustTransport? _transport;
final String peerPubkey;
final String selfPubkey;
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".
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, and whether they're in your circle.
Future<void> load() async {
final transport = _transport;
if (transport == null) {
emit(state.copyWith(loading: false));
return;
}
emit(state.copyWith(loading: true));
final wot = WebOfTrust.fromCertifications(
await transport.allCertifications(),
now: DateTime.now(),
);
final certifiers = wot.certifiersOf(peerPubkey);
final circle = wot.members(
seeds: {selfPubkey},
threshold: _circleThreshold,
maxDistance: _circleDistance,
);
emit(state.copyWith(
certifierCount: certifiers.length,
iVouch: certifiers.contains(selfPubkey),
knownToYou: circle.contains(peerPubkey),
loading: false,
));
}
/// Adds or removes this user's vouch, then reloads. Never vouches for self.
/// 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;
emit(state.copyWith(busy: true));
if (state.iVouch) {
await transport.revoke(subjectPubkey: peerPubkey);
} else {
await transport.certify(
subjectPubkey: peerPubkey,
validity: _vouchValidity,
);
}
await load();
emit(state.copyWith(busy: false));
}
@override
Future<void> close() async {
await _onDispose?.call();
return super.close();
}
}