193 lines
6.1 KiB
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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|