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.
This commit is contained in:
parent
4326e79419
commit
7cba4f7fcf
10 changed files with 367 additions and 17 deletions
121
apps/app_seeds/lib/state/trust_cubit.dart
Normal file
121
apps/app_seeds/lib/state/trust_cubit.dart
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue