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.
This commit is contained in:
parent
4af90876f4
commit
dc7b2eee27
25 changed files with 595 additions and 863 deletions
240
apps/app_seeds/lib/ui/your_people_screen.dart
Normal file
240
apps/app_seeds/lib/ui/your_people_screen.dart
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue