View all abuse reports

This commit is contained in:
LittleSheep 2024-12-08 14:44:55 +08:00
parent e6a9185d11
commit add904cc41
9 changed files with 533 additions and 95 deletions

View File

@ -14,6 +14,7 @@
"screenAccountPublisherNew": "New Publisher",
"screenAccountPublisherEdit": "Edit Publisher",
"screenAccountProfileEdit": "Edit Profile",
"screenAbuseReport": "Abuse Reports",
"screenSettings": "Settings",
"screenAlbum": "Album",
"screenChat": "Chat",
@ -395,6 +396,7 @@
"postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
"abuseReport": "Abuse Report",
"abuseReportDescription": "Report any resources that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe the location of the resource (provide resource ID as best as possible) and how this violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
"abuseReportAction": "Submit Abuse Report",
"abuseReportActionDescription": "Report abuse usage behavior.",
"abuseReportResource": "Resource Location / ID",
"abuseReportReason": "Reason",

View File

@ -14,6 +14,7 @@
"screenAccountPublisherNew": "新建发布者",
"screenAccountPublisherEdit": "编辑发布者",
"screenAccountProfileEdit": "编辑资料",
"screenAbuseReport": "滥用检举",
"screenSettings": "设置",
"screenAlbum": "相册",
"screenChat": "聊天",
@ -395,6 +396,7 @@
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
"abuseReport": "检举",
"abuseReportDescription": "检举不符合我们用户协议以及社区准则的任何资源,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述资源的位置(提供资源 ID 为佳)以及如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
"abuseReportAction": "提交检举",
"abuseReportActionDescription": "检举不合规行为。",
"abuseReportResource": "资源位置 / ID",
"abuseReportReason": "检举原因",

View File

@ -1,6 +1,7 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:surface/screens/abuse_report.dart';
import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/pfp.dart';
import 'package:surface/screens/account/profile_edit.dart';
@ -242,6 +243,13 @@ final _appRoutes = [
child: RegisterScreen(),
),
),
GoRoute(
path: '/reports',
name: 'abuseReport',
builder: (context, state) => const AppBackground(
child: AbuseReportScreen(),
),
),
GoRoute(
path: '/account/profile/edit',
name: 'accountProfileEdit',

View File

@ -0,0 +1,178 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/dialog.dart';
import '../types/account.dart';
class AbuseReportScreen extends StatefulWidget {
const AbuseReportScreen({super.key});
@override
State<AbuseReportScreen> createState() => _AbuseReportScreenState();
}
class _AbuseReportScreenState extends State<AbuseReportScreen> {
bool _isBusy = false;
List<SnAbuseReport> _reports = List.empty();
Future<void> _fetchReports() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/reports/abuse');
if (!mounted) return;
_reports = resp.data.map((e) => SnAbuseReport.fromJson(e)).cast<SnAbuseReport>().toList();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
void _showAbuseReportDialog() {
showDialog(
context: context,
builder: (context) => _AbuseReportDialog(),
).then((value) {
if (value == true && mounted) {
_fetchReports();
context.showSnackbar('abuseReportSubmitted'.tr());
}
});
}
@override
void initState() {
super.initState();
_fetchReports();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
ListTile(
title: Text('abuseReportAction').tr(),
subtitle: Text('abuseReportActionDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.report),
trailing: const Icon(Icons.chevron_right),
onTap: _showAbuseReportDialog,
),
const Divider(height: 1),
if (_isBusy)
const CircularProgressIndicator().padding(all: 24).center()
else
Expanded(
child: ListView.builder(
itemCount: _reports.length,
itemBuilder: (context, idx) {
return ListTile(
isThreeLine: true,
title: Text(_reports[idx].resource, style: GoogleFonts.robotoMono(fontSize: 13)),
subtitle: Text(_reports[idx].reason),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.flag),
);
},
),
),
],
),
);
}
}
class _AbuseReportDialog extends StatefulWidget {
const _AbuseReportDialog({super.key});
@override
State<_AbuseReportDialog> createState() => _AbuseReportDialogState();
}
class _AbuseReportDialogState extends State<_AbuseReportDialog> {
bool _isBusy = false;
final _resourceController = TextEditingController();
final _reasonController = TextEditingController();
@override
dispose() {
_resourceController.dispose();
_reasonController.dispose();
super.dispose();
}
Future<void> _performAction() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post(
'/cgi/id/reports/abuse',
data: {
'resource': _resourceController.text,
'reason': _reasonController.text,
},
);
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('abuseReport'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('abuseReportDescription'.tr()),
const Gap(12),
TextField(
controller: _resourceController,
maxLength: null,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'abuseReportResource'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _reasonController,
maxLength: null,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'abuseReportReason'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isBusy ? null : () => Navigator.pop(context),
child: Text('dialogDismiss').tr(),
),
TextButton(
onPressed: _isBusy ? null : _performAction,
child: Text('submit').tr(),
),
],
);
}
}

View File

@ -107,14 +107,7 @@ class _AuthorizedAccountScreen extends StatelessWidget {
leading: const Icon(Symbols.flag),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (context) => _AbuseReportDialog(),
).then((value) {
if (value == true && context.mounted) {
context.showSnackbar('abuseReportSubmitted'.tr());
}
});
GoRouter.of(context).pushNamed('abuseReport');
},
),
ListTile(
@ -221,89 +214,3 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
);
}
}
class _AbuseReportDialog extends StatefulWidget {
const _AbuseReportDialog({super.key});
@override
State<_AbuseReportDialog> createState() => _AbuseReportDialogState();
}
class _AbuseReportDialogState extends State<_AbuseReportDialog> {
bool _isBusy = false;
final _resourceController = TextEditingController();
final _reasonController = TextEditingController();
@override
dispose() {
_resourceController.dispose();
_reasonController.dispose();
super.dispose();
}
Future<void> _performAction() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post(
'/cgi/id/reports/abuse',
data: {
'resource': _resourceController.text,
'reason': _reasonController.text,
},
);
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('abuseReport'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('abuseReportDescription'.tr()),
const Gap(12),
TextField(
controller: _resourceController,
maxLength: null,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'abuseReportResource'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _reasonController,
maxLength: null,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'abuseReportReason'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isBusy ? null : () => Navigator.pop(context),
child: Text('dialogDismiss').tr(),
),
TextButton(
onPressed: _isBusy ? null : _performAction,
child: Text('submit').tr(),
),
],
);
}
}

View File

@ -5,7 +5,6 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/post.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart';

View File

@ -119,3 +119,20 @@ class SnAccountStatusInfo with _$SnAccountStatusInfo {
factory SnAccountStatusInfo.fromJson(Map<String, Object?> json) =>
_$SnAccountStatusInfoFromJson(json);
}
@freezed
class SnAbuseReport with _$SnAbuseReport {
const factory SnAbuseReport({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String resource,
required String reason,
required String status,
required int accountId,
}) = _SnAbuseReport;
factory SnAbuseReport.fromJson(Map<String, Object?> json) =>
_$SnAbuseReportFromJson(json);
}

View File

@ -2205,3 +2205,302 @@ abstract class _SnAccountStatusInfo implements SnAccountStatusInfo {
_$$SnAccountStatusInfoImplCopyWith<_$SnAccountStatusInfoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) {
return _SnAbuseReport.fromJson(json);
}
/// @nodoc
mixin _$SnAbuseReport {
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
DateTime? get deletedAt => throw _privateConstructorUsedError;
String get resource => throw _privateConstructorUsedError;
String get reason => throw _privateConstructorUsedError;
String get status => throw _privateConstructorUsedError;
int get accountId => throw _privateConstructorUsedError;
/// Serializes this SnAbuseReport to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnAbuseReportCopyWith<SnAbuseReport> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnAbuseReportCopyWith<$Res> {
factory $SnAbuseReportCopyWith(
SnAbuseReport value, $Res Function(SnAbuseReport) then) =
_$SnAbuseReportCopyWithImpl<$Res, SnAbuseReport>;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String resource,
String reason,
String status,
int accountId});
}
/// @nodoc
class _$SnAbuseReportCopyWithImpl<$Res, $Val extends SnAbuseReport>
implements $SnAbuseReportCopyWith<$Res> {
_$SnAbuseReportCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? resource = null,
Object? reason = null,
Object? status = null,
Object? accountId = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
resource: null == resource
? _value.resource
: resource // ignore: cast_nullable_to_non_nullable
as String,
reason: null == reason
? _value.reason
: reason // ignore: cast_nullable_to_non_nullable
as String,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnAbuseReportImplCopyWith<$Res>
implements $SnAbuseReportCopyWith<$Res> {
factory _$$SnAbuseReportImplCopyWith(
_$SnAbuseReportImpl value, $Res Function(_$SnAbuseReportImpl) then) =
__$$SnAbuseReportImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String resource,
String reason,
String status,
int accountId});
}
/// @nodoc
class __$$SnAbuseReportImplCopyWithImpl<$Res>
extends _$SnAbuseReportCopyWithImpl<$Res, _$SnAbuseReportImpl>
implements _$$SnAbuseReportImplCopyWith<$Res> {
__$$SnAbuseReportImplCopyWithImpl(
_$SnAbuseReportImpl _value, $Res Function(_$SnAbuseReportImpl) _then)
: super(_value, _then);
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? resource = null,
Object? reason = null,
Object? status = null,
Object? accountId = null,
}) {
return _then(_$SnAbuseReportImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
resource: null == resource
? _value.resource
: resource // ignore: cast_nullable_to_non_nullable
as String,
reason: null == reason
? _value.reason
: reason // ignore: cast_nullable_to_non_nullable
as String,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnAbuseReportImpl implements _SnAbuseReport {
const _$SnAbuseReportImpl(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.resource,
required this.reason,
required this.status,
required this.accountId});
factory _$SnAbuseReportImpl.fromJson(Map<String, dynamic> json) =>
_$$SnAbuseReportImplFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final String resource;
@override
final String reason;
@override
final String status;
@override
final int accountId;
@override
String toString() {
return 'SnAbuseReport(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resource: $resource, reason: $reason, status: $status, accountId: $accountId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnAbuseReportImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.resource, resource) ||
other.resource == resource) &&
(identical(other.reason, reason) || other.reason == reason) &&
(identical(other.status, status) || other.status == status) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, resource, reason, status, accountId);
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnAbuseReportImplCopyWith<_$SnAbuseReportImpl> get copyWith =>
__$$SnAbuseReportImplCopyWithImpl<_$SnAbuseReportImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnAbuseReportImplToJson(
this,
);
}
}
abstract class _SnAbuseReport implements SnAbuseReport {
const factory _SnAbuseReport(
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final String resource,
required final String reason,
required final String status,
required final int accountId}) = _$SnAbuseReportImpl;
factory _SnAbuseReport.fromJson(Map<String, dynamic> json) =
_$SnAbuseReportImpl.fromJson;
@override
int get id;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
DateTime? get deletedAt;
@override
String get resource;
@override
String get reason;
@override
String get status;
@override
int get accountId;
/// Create a copy of SnAbuseReport
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnAbuseReportImplCopyWith<_$SnAbuseReportImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -212,3 +212,29 @@ Map<String, dynamic> _$$SnAccountStatusInfoImplToJson(
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
'status': instance.status,
};
_$SnAbuseReportImpl _$$SnAbuseReportImplFromJson(Map<String, dynamic> json) =>
_$SnAbuseReportImpl(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
resource: json['resource'] as String,
reason: json['reason'] as String,
status: json['status'] as String,
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$$SnAbuseReportImplToJson(_$SnAbuseReportImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'resource': instance.resource,
'reason': instance.reason,
'status': instance.status,
'account_id': instance.accountId,
};