tane/apps/app_seeds/lib/ui/report_sheet.dart

193 lines
6.1 KiB
Dart

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<ReportOutcome?> showReportSheet(
BuildContext context, {
required SocialConnection connection,
required String subjectPubkey,
String? offerAddress,
SocialSettings? settings,
}) {
return showModalBottomSheet<ReportOutcome>(
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<ReportSheet> createState() => _ReportSheetState();
}
class _ReportSheetState extends State<ReportSheet> {
ReportReason _reason = ReportReason.spam;
bool _alsoBlock = false;
bool _sending = false;
final _details = TextEditingController();
@override
void dispose() {
_details.dispose();
super.dispose();
}
Future<void> _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<ReportReason>(
groupValue: _reason,
onChanged: _sending
? (_) {}
: (v) => setState(() => _reason = v ?? _reason),
child: Column(
children: [
for (final (value, label) in reasons)
RadioListTile<ReportReason>(
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),
),
),
],
),
),
);
}
}