import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; 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 /// metadata-hiding (NIP-17) under the hood; local-first, so it degrades to a /// "set up sharing" note when there's no relay. class ChatScreen extends StatefulWidget { const ChatScreen({ required this.social, required this.settings, required this.peerPubkey, super.key, }); final SocialService social; final SocialSettings settings; final String peerPubkey; @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State { SocialSession? _session; MessagesCubit? _messages; TrustCubit? _trust; bool _loading = true; final _input = TextEditingController(); @override void initState() { super.initState(); _init(); } Future _init() async { // 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 session?.close(); return; } 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(() { _session = session; _messages = messages; _trust = trust; _loading = false; }); } Future _send() async { final cubit = _messages; if (cubit == null || _input.text.trim().isEmpty) return; final text = _input.text; _input.clear(); await cubit.send(text); } @override void dispose() { _messages?.close(); _trust?.close(); _session?.close(); _input.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final t = context.t; final messages = _messages; final trust = _trust; return Scaffold( appBar: AppBar(title: Text(t.chat.title)), body: _loading || messages == null || trust == null ? const Center(child: CircularProgressIndicator()) : MultiBlocProvider( providers: [ BlocProvider.value(value: messages), BlocProvider.value(value: trust), ], child: _ChatBody(controller: _input, onSend: _send), ), ); } } class _ChatBody extends StatelessWidget { const _ChatBody({required this.controller, required this.onSend}); final TextEditingController controller; final Future Function() onSend; @override Widget build(BuildContext context) { final t = context.t; final cubit = context.read(); if (!cubit.isOnline) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Text( t.chat.offline, textAlign: TextAlign.center, style: const TextStyle(color: seedMuted, fontSize: 15), ), ), ); } return Column( children: [ const _TrustBanner(), Expanded( child: BlocBuilder( builder: (context, state) { if (state.messages.isEmpty) { return Center( child: Text(t.chat.empty, style: const TextStyle(color: seedMuted)), ); } return ListView.builder( padding: const EdgeInsets.all(12), itemCount: state.messages.length, itemBuilder: (context, i) { final m = state.messages[i]; return _Bubble(text: m.text, mine: cubit.isMine(m)); }, ); }, ), ), _Composer(controller: controller, onSend: onSend), ], ); } } /// 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( builder: (context, state) { final cubit = context.read(); 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}); final String text; final bool mine; @override Widget build(BuildContext context) { return Align( alignment: mine ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart, child: Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.75, ), decoration: BoxDecoration( color: mine ? seedGreen : seedPrimaryContainer, borderRadius: BorderRadius.circular(16), ), child: Text( text, style: TextStyle( color: mine ? Colors.white : seedOnSurface, fontSize: 15, ), ), ), ); } } class _Composer extends StatelessWidget { const _Composer({required this.controller, required this.onSend}); final TextEditingController controller; final Future Function() onSend; @override Widget build(BuildContext context) { final t = context.t; return SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(12, 6, 12, 12), child: Row( children: [ Expanded( child: TextField( key: const Key('chat.input'), controller: controller, textInputAction: TextInputAction.send, onSubmitted: (_) => onSend(), decoration: InputDecoration( hintText: t.chat.hint, filled: true, fillColor: seedField, border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), ), ), ), const SizedBox(width: 8), IconButton.filled( key: const Key('chat.send'), icon: const Icon(Icons.send), tooltip: t.chat.send, onPressed: onSend, ), ], ), ), ); } }