tane/apps/app_seeds/lib/state/trust_cubit.dart
vjrj 7cba4f7fcf feat(block2): trust UI — vouch for a person in chat (web of trust)
- TrustCubit: reads how many vouch for the peer + whether YOU do; toggleVouch
  certifies/revokes over TrustTransport; never vouches for self; offline-safe.
  Kept simple — the full known-member rule (threshold+distance) and its
  bootstrap set are still-open policy (network-trust §2); a live count + your
  vouch is the honest first step.
- Chat screen now opens ONE session carrying both messaging AND trust (the
  shared-connection shape), and shows a slim vouch banner: 'Vouched for by N' +
  an 'I know this person' / 'You vouch for them' toggle.
- i18n en/es/pt (human words: 'I know this person', not 'certify').

Tests: 4 trust cubit + messaging + chat, 10 green; analyzer clean.
2026-07-10 11:04:07 +02:00

121 lines
3.4 KiB
Dart

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.loading = true,
this.busy = false,
});
final int certifierCount;
final bool iVouch;
final bool loading;
final bool busy;
TrustState copyWith({
int? certifierCount,
bool? iVouch,
bool? loading,
bool? busy,
}) =>
TrustState(
certifierCount: certifierCount ?? this.certifierCount,
iVouch: iVouch ?? this.iVouch,
loading: loading ?? this.loading,
busy: busy ?? this.busy,
);
@override
List<Object?> get props => [certifierCount, iVouch, loading, busy];
}
/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport].
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;
/// Loads the current certifiers of the peer.
Future<void> load() async {
final transport = _transport;
if (transport == null) {
emit(state.copyWith(loading: false));
return;
}
emit(state.copyWith(loading: true));
final certifiers = await transport.certifiersOf(peerPubkey);
emit(state.copyWith(
certifierCount: certifiers.length,
iVouch: certifiers.contains(selfPubkey),
loading: false,
));
}
/// Adds or removes this user's vouch, then reloads. Never vouches for self.
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);
}
await load();
emit(state.copyWith(busy: false));
}
@override
Future<void> close() async {
await _onDispose?.call();
return super.close();
}
}
/// Opens a [TrustCubit] wired to the social layer for [peerPubkey], or an
/// offline one.
Future<TrustCubit> 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);
}
}