feat(social): standard NIP-56 reporting — ReportTransport in commons_core, report sheet on offers and chats, local hide + optional block
This commit is contained in:
parent
11b242edbf
commit
0e7faaeb90
15 changed files with 560 additions and 15 deletions
193
apps/app_seeds/lib/ui/report_sheet.dart
Normal file
193
apps/app_seeds/lib/ui/report_sheet.dart
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue