feat(trust): full Duniter web-of-trust membership (params + referents)
Slice 4 of Block 2. The pure rule was already parameterised; this adds the policy, cold-start and UI around it. - commons_core: WotParams (sigQty/stepMax/sigValidity, Duniter defaults) + WebOfTrust.membersWith; npubToHex helper (NIP-19 decode) for adding roots. - WotSettings (keystore): the active parameters, configurable — a young network loosens them and tightens as it grows, as Ğ1 did. Defaults Duniter. - TrustReferents: the bootstrap 'seeds' membership is measured from — a bundled asset (empty until real founders are curated, no invented keys) unioned with referents the user adds by npub/QR. Honest cold-start. - TrustCubit: computes the full membership verdict against referents+params alongside the personal circle, and exposes a TrustTier (networkMember > inYourCircle > vouched > unknown). Certifications issued with the active validity (they expire and renew, Duniter rule). - UI: chat trust badge by tier; a 'Network of trust' screen (manage roots + advanced params) reached from the profile. i18n en/es/pt/ast. Tests: WotParams/membersWith, npubToHex, TrustReferents, WotSettings, and TrustCubit tiers/membership. Resolves the WoT-parameters decision (open-decisions §B). Trust net stays empty/undetermined until seeded — by design; users bootstrap their own roots.
This commit is contained in:
parent
f767ccd70e
commit
e715b129f6
29 changed files with 1111 additions and 39 deletions
228
apps/app_seeds/lib/ui/trust_network_screen.dart
Normal file
228
apps/app_seeds/lib/ui/trust_network_screen.dart
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue