import 'package:commons_core/commons_core.dart'; import 'package:flutter/material.dart'; import '../i18n/strings.g.dart'; import '../services/social_connection.dart'; import '../services/social_settings.dart'; import 'theme.dart'; /// What a submitted report did, so the caller can react (hide the offer, /// leave the chat when the person was also blocked). typedef ReportOutcome = ({bool sent, bool blocked}); /// Opens the report sheet for a person (and optionally one of their offers, /// via [offerAddress] `kind:pubkey:d`). Publishes a standard report to the /// user's community servers and, when ticked, also blocks the person locally. /// Returns null when dismissed without sending. Future showReportSheet( BuildContext context, { required SocialConnection connection, required String subjectPubkey, String? offerAddress, SocialSettings? settings, }) { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => ReportSheet( connection: connection, subjectPubkey: subjectPubkey, offerAddress: offerAddress, settings: settings, ), ); } /// The report form: what's wrong (mapped onto the standard report categories), /// optional details, and an optional "also block" tick. class ReportSheet extends StatefulWidget { const ReportSheet({ required this.connection, required this.subjectPubkey, this.offerAddress, this.settings, super.key, }); final SocialConnection connection; final String subjectPubkey; /// When reporting one offer (not just the person): its address. final String? offerAddress; /// Enables the "also block this person" option; null hides it. final SocialSettings? settings; @override State createState() => _ReportSheetState(); } class _ReportSheetState extends State { ReportReason _reason = ReportReason.spam; bool _alsoBlock = false; bool _sending = false; final _details = TextEditingController(); @override void dispose() { _details.dispose(); super.dispose(); } Future _submit() async { final t = context.t; final messenger = ScaffoldMessenger.of(context); setState(() => _sending = true); try { final session = await widget.connection.session(); if (session == null) throw StateError('offline'); await session.reports.report( subjectPubkey: widget.subjectPubkey, eventAddress: widget.offerAddress, reason: _reason, details: _details.text.trim(), ); var blocked = false; if (_alsoBlock && widget.settings != null) { await widget.settings!.block(widget.subjectPubkey); blocked = true; } if (!mounted) return; messenger ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(t.report.sentHidden))); Navigator.of(context).pop((sent: true, blocked: blocked)); } catch (_) { if (!mounted) return; setState(() => _sending = false); messenger ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(t.report.failed))); } } @override Widget build(BuildContext context) { final t = context.t; final reasons = <(ReportReason, String)>[ (ReportReason.spam, t.report.reasonSpam), (ReportReason.profanity, t.report.reasonAbuse), (ReportReason.illegal, t.report.reasonIllegal), (ReportReason.other, t.report.reasonOther), ]; return SafeArea( child: SingleChildScrollView( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: 16 + MediaQuery.of(context).viewInsets.bottom, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( t.report.title, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: seedOnSurface, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( t.report.prompt, style: const TextStyle(color: seedMuted, fontSize: 13), ), const SizedBox(height: 4), RadioGroup( groupValue: _reason, onChanged: _sending ? (_) {} : (v) => setState(() => _reason = v ?? _reason), child: Column( children: [ for (final (value, label) in reasons) RadioListTile( value: value, title: Text(label), dense: true, contentPadding: EdgeInsets.zero, ), ], ), ), TextField( controller: _details, enabled: !_sending, minLines: 1, maxLines: 3, decoration: InputDecoration(hintText: t.report.detailsHint), ), if (widget.settings != null) CheckboxListTile( value: _alsoBlock, onChanged: _sending ? null : (v) => setState(() => _alsoBlock = v ?? false), title: Text(t.report.alsoBlock), dense: true, contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, ), const SizedBox(height: 8), Align( alignment: AlignmentDirectional.centerEnd, child: FilledButton( key: const Key('report.send'), onPressed: _sending ? null : _submit, child: _sending ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(t.report.send), ), ), ], ), ), ); } }