import 'package:commons_core/commons_core.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../services/social_service.dart'; import '../services/social_settings.dart'; /// Trust standing of one peer: how many people vouch for them, and whether YOU /// do. Kept simple on purpose — the full "known member" rule (threshold + /// distance) and its bootstrap set are a policy decision still open /// (network-trust.md §2). Vouching + a live count is the useful, honest first /// step. 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) 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 of trust — you vouch for /// them, or someone you vouch for does (friend-of-a-friend). This is the /// meaningful signal; a raw network count invites spam. final bool knownToYou; final bool loading; final bool busy; 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 get props => [certifierCount, iVouch, knownToYou, loading, busy]; } /// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport]. class TrustCubit extends Cubit { TrustCubit( this._transport, { required this.peerPubkey, required this.selfPubkey, Future Function()? onDispose, }) : _onDispose = onDispose, super(const TrustState()); final TrustTransport? _transport; final String peerPubkey; final String selfPubkey; final Future Function()? _onDispose; bool get isOnline => _transport != null; /// Bootstrap "circle" rule: one vouch from your side, out to a friend-of-a- /// friend. Deliberately loose (the full member policy — threshold/distance — /// is still open, network-trust.md §2); this is just "people near you". static const _circleThreshold = 1; static const _circleDistance = 2; /// Loads the peer's certifiers and whether they're within your circle. Future 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. Future 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); } await load(); emit(state.copyWith(busy: false)); } @override Future close() async { await _onDispose?.call(); return super.close(); } } /// Opens a [TrustCubit] wired to the social layer for [peerPubkey], or an /// offline one. Future createTrustCubit( SocialService social, SocialSettings settings, { required String peerPubkey, }) async { final relays = await settings.relayUrls(); if (relays.isEmpty) { return TrustCubit(null, peerPubkey: peerPubkey, selfPubkey: social.publicKeyHex); } try { final session = await social.openSession(relays); return TrustCubit( session.trust, peerPubkey: peerPubkey, selfPubkey: social.publicKeyHex, onDispose: session.close, ); } catch (_) { return TrustCubit(null, peerPubkey: peerPubkey, selfPubkey: social.publicKeyHex); } }