tane/apps/app_seeds/lib/ui/your_people_screen.dart
vjrj dc7b2eee27 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

240 lines
7 KiB
Dart

import 'dart:async';
import 'package:commons_core/commons_core.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../i18n/strings.g.dart';
import '../services/profile_cache.dart';
import '../services/social_connection.dart';
import '../services/social_service.dart';
import 'theme.dart';
/// "Your people": who you vouch for and who vouches for you, in human
/// language. Ego-centric by design — no roots, no parameters, no graph talk.
/// Reached from the profile; rows open the 1:1 chat.
class YourPeopleScreen extends StatefulWidget {
const YourPeopleScreen({
required this.social,
required this.connection,
this.profileCache,
super.key,
});
final SocialService social;
/// The shared relay connection (one per identity), carrying trust.
final SocialConnection connection;
/// Optional cache of peer display names; null in tests.
final ProfileCache? profileCache;
@override
State<YourPeopleScreen> createState() => _YourPeopleScreenState();
}
class _YourPeopleScreenState extends State<YourPeopleScreen> {
TrustTransport? _transport;
bool _loading = true;
bool _busy = false;
List<String> _youVouchFor = const [];
List<String> _vouchForYou = const [];
final Map<String, String> _names = {};
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
final session = await widget.connection.session();
if (!mounted) return; // shared session is owned by the connection, not us
_transport = session?.trust;
await _load();
}
Future<void> _load() async {
final transport = _transport;
if (transport == null) {
if (mounted) setState(() => _loading = false);
return;
}
final self = widget.social.publicKeyHex;
final now = DateTime.now();
final certs = (await transport.allCertifications())
.where((c) => c.isValidAt(now))
.toList();
final given = [
for (final c in certs)
if (c.issuer == self) c.subject,
]..sort();
final received = [
for (final c in certs)
if (c.subject == self) c.issuer,
]..sort();
final cache = widget.profileCache;
if (cache != null) {
for (final peer in {...given, ...received}) {
final name = await cache.name(peer);
if (name != null) _names[peer] = name;
}
}
if (!mounted) return;
setState(() {
_youVouchFor = given;
_vouchForYou = received;
_loading = false;
});
}
Future<void> _revoke(String peer) async {
final t = context.t;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(t.yourPeople.revokeConfirm),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(t.common.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text(t.yourPeople.revoke),
),
],
),
);
final transport = _transport;
if (confirmed != true || transport == null || !mounted) return;
setState(() => _busy = true);
await transport.revoke(subjectPubkey: peer);
await _load();
if (mounted) setState(() => _busy = false);
}
@override
Widget build(BuildContext context) {
final t = context.t;
return Scaffold(
appBar: AppBar(title: Text(t.yourPeople.title)),
body: _loading
? const Center(child: CircularProgressIndicator())
: _transport == null
? Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Text(
t.yourPeople.offline,
textAlign: TextAlign.center,
style: const TextStyle(color: seedMuted, fontSize: 15),
),
),
)
: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(
16, 8, 16, 8),
child: Text(
t.yourPeople.help,
style:
const TextStyle(color: seedMuted, fontSize: 14),
),
),
_SectionHeader(label: t.yourPeople.youVouchFor),
if (_youVouchFor.isEmpty)
_EmptyNote(text: t.yourPeople.youVouchForEmpty)
else
for (final peer in _youVouchFor)
_PersonTile(
key: Key('yourPeople.given.$peer'),
name: _names[peer] ?? shortPubkey(peer),
onOpen: () => context.push('/chat/$peer'),
trailing: TextButton(
onPressed: _busy ? null : () => _revoke(peer),
child: Text(t.yourPeople.revoke),
),
),
const SizedBox(height: 12),
_SectionHeader(label: t.yourPeople.vouchesForYou),
if (_vouchForYou.isEmpty)
_EmptyNote(text: t.yourPeople.vouchesForYouEmpty)
else
for (final peer in _vouchForYou)
_PersonTile(
key: Key('yourPeople.received.$peer'),
name: _names[peer] ?? shortPubkey(peer),
onOpen: () => context.push('/chat/$peer'),
),
],
),
);
}
}
class _SectionHeader extends StatelessWidget {
const _SectionHeader({required this.label});
final String label;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 12, 16, 4),
child: Text(
label,
style: const TextStyle(
color: seedGreen,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _EmptyNote extends StatelessWidget {
const _EmptyNote({required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 4, 16, 4),
child: Text(
text,
style: const TextStyle(color: seedMuted, fontSize: 14),
),
);
}
}
class _PersonTile extends StatelessWidget {
const _PersonTile({
required this.name,
required this.onOpen,
this.trailing,
super.key,
});
final String name;
final VoidCallback onOpen;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return ListTile(
leading: const CircleAvatar(
backgroundColor: seedAvatar,
child: Icon(Icons.person, color: seedOnAvatar),
),
title: Text(name),
trailing: trailing,
onTap: onOpen,
);
}
}