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 createState() => _YourPeopleScreenState(); } class _YourPeopleScreenState extends State { TrustTransport? _transport; bool _loading = true; bool _busy = false; List _youVouchFor = const []; List _vouchForYou = const []; final Map _names = {}; @override void initState() { super.initState(); _init(); } Future _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 _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 _revoke(String peer) async { final t = context.t; final confirmed = await showDialog( 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, ); } }