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
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
|
|
@ -5,6 +7,7 @@ import '../i18n/strings.g.dart';
|
|||
import '../services/social_service.dart';
|
||||
import '../services/social_settings.dart';
|
||||
import '../state/messages_cubit.dart';
|
||||
import '../state/trust_cubit.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// A 1:1 encrypted chat with one peer (redesign screen 10). Private and
|
||||
|
|
@ -27,7 +30,9 @@ class ChatScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
MessagesCubit? _cubit;
|
||||
SocialSession? _session;
|
||||
MessagesCubit? _messages;
|
||||
TrustCubit? _trust;
|
||||
bool _loading = true;
|
||||
final _input = TextEditingController();
|
||||
|
||||
|
|
@ -38,24 +43,38 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
final cubit = await createMessagesCubit(
|
||||
widget.social,
|
||||
widget.settings,
|
||||
peerPubkey: widget.peerPubkey,
|
||||
);
|
||||
// One session for this chat carries both messaging and trust (the shared-
|
||||
// connection shape). Offline (no relays / unreachable) → null transports.
|
||||
final relays = await widget.settings.relayUrls();
|
||||
SocialSession? session;
|
||||
if (relays.isNotEmpty) {
|
||||
try {
|
||||
session = await widget.social.openSession(relays);
|
||||
} catch (_) {
|
||||
session = null;
|
||||
}
|
||||
}
|
||||
if (!mounted) {
|
||||
await cubit.close();
|
||||
await session?.close();
|
||||
return;
|
||||
}
|
||||
cubit.start();
|
||||
final self = widget.social.publicKeyHex;
|
||||
final messages = MessagesCubit(session?.messages,
|
||||
peerPubkey: widget.peerPubkey, selfPubkey: self)
|
||||
..start();
|
||||
final trust = TrustCubit(session?.trust,
|
||||
peerPubkey: widget.peerPubkey, selfPubkey: self);
|
||||
unawaited(trust.load());
|
||||
setState(() {
|
||||
_cubit = cubit;
|
||||
_session = session;
|
||||
_messages = messages;
|
||||
_trust = trust;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _send() async {
|
||||
final cubit = _cubit;
|
||||
final cubit = _messages;
|
||||
if (cubit == null || _input.text.trim().isEmpty) return;
|
||||
final text = _input.text;
|
||||
_input.clear();
|
||||
|
|
@ -64,7 +83,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_cubit?.close();
|
||||
_messages?.close();
|
||||
_trust?.close();
|
||||
_session?.close();
|
||||
_input.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
@ -72,13 +93,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
final cubit = _cubit;
|
||||
final messages = _messages;
|
||||
final trust = _trust;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(t.chat.title)),
|
||||
body: _loading || cubit == null
|
||||
body: _loading || messages == null || trust == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: BlocProvider.value(
|
||||
value: cubit,
|
||||
: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: messages),
|
||||
BlocProvider.value(value: trust),
|
||||
],
|
||||
child: _ChatBody(controller: _input, onSend: _send),
|
||||
),
|
||||
);
|
||||
|
|
@ -109,6 +134,7 @@ class _ChatBody extends StatelessWidget {
|
|||
}
|
||||
return Column(
|
||||
children: [
|
||||
const _TrustBanner(),
|
||||
Expanded(
|
||||
child: BlocBuilder<MessagesCubit, ChatState>(
|
||||
builder: (context, state) {
|
||||
|
|
@ -135,6 +161,47 @@ class _ChatBody extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// A slim "web of trust" strip: how many vouch for this peer, and a toggle to
|
||||
/// add/remove your own vouch ("I know this person").
|
||||
class _TrustBanner extends StatelessWidget {
|
||||
const _TrustBanner();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
return BlocBuilder<TrustCubit, TrustState>(
|
||||
builder: (context, state) {
|
||||
final cubit = context.read<TrustCubit>();
|
||||
if (!cubit.isOnline || state.loading) return const SizedBox.shrink();
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: seedPrimaryContainer.withValues(alpha: 0.5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.verified_user_outlined, size: 18, color: seedGreen),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.certifierCount == 0
|
||||
? t.trust.none
|
||||
: t.trust.count(n: state.certifierCount),
|
||||
style: const TextStyle(color: seedOnSurface, fontSize: 13),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
key: const Key('chat.vouch'),
|
||||
onPressed: state.busy ? null : cubit.toggleVouch,
|
||||
child: Text(state.iVouch ? t.trust.vouched : t.trust.vouch),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Bubble extends StatelessWidget {
|
||||
const _Bubble({required this.text, required this.mine});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue