From add904cc41e607323658532672ea823828388802 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 8 Dec 2024 14:44:55 +0800 Subject: [PATCH] :sparkles: View all abuse reports --- assets/translations/en.json | 2 + assets/translations/zh.json | 2 + lib/router.dart | 8 + lib/screens/abuse_report.dart | 178 ++++++++++++++++++ lib/screens/account.dart | 95 +--------- lib/screens/post/post_search.dart | 1 - lib/types/account.dart | 17 ++ lib/types/account.freezed.dart | 299 ++++++++++++++++++++++++++++++ lib/types/account.g.dart | 26 +++ 9 files changed, 533 insertions(+), 95 deletions(-) create mode 100644 lib/screens/abuse_report.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 5ab56e8..52625fe 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -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", diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 91cc45d..b12534a 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -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": "检举原因", diff --git a/lib/router.dart b/lib/router.dart index 7d70bb3..79d67c4 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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', diff --git a/lib/screens/abuse_report.dart b/lib/screens/abuse_report.dart new file mode 100644 index 0000000..ce8188b --- /dev/null +++ b/lib/screens/abuse_report.dart @@ -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 createState() => _AbuseReportScreenState(); +} + +class _AbuseReportScreenState extends State { + bool _isBusy = false; + + List _reports = List.empty(); + + Future _fetchReports() async { + setState(() => _isBusy = true); + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/id/reports/abuse'); + if (!mounted) return; + _reports = resp.data.map((e) => SnAbuseReport.fromJson(e)).cast().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 _performAction() async { + setState(() => _isBusy = true); + try { + final sn = context.read(); + 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(), + ), + ], + ); + } +} diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 71bdfdc..9099fcd 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -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 _performAction() async { - setState(() => _isBusy = true); - try { - final sn = context.read(); - 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(), - ), - ], - ); - } -} diff --git a/lib/screens/post/post_search.dart b/lib/screens/post/post_search.dart index 00774f7..9dc1b1b 100644 --- a/lib/screens/post/post_search.dart +++ b/lib/screens/post/post_search.dart @@ -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'; diff --git a/lib/types/account.dart b/lib/types/account.dart index 9fa02cf..8d0357e 100644 --- a/lib/types/account.dart +++ b/lib/types/account.dart @@ -119,3 +119,20 @@ class SnAccountStatusInfo with _$SnAccountStatusInfo { factory SnAccountStatusInfo.fromJson(Map 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 json) => + _$SnAbuseReportFromJson(json); +} diff --git a/lib/types/account.freezed.dart b/lib/types/account.freezed.dart index 69fcdd8..8f3eefc 100644 --- a/lib/types/account.freezed.dart +++ b/lib/types/account.freezed.dart @@ -2205,3 +2205,302 @@ abstract class _SnAccountStatusInfo implements SnAccountStatusInfo { _$$SnAccountStatusInfoImplCopyWith<_$SnAccountStatusInfoImpl> get copyWith => throw _privateConstructorUsedError; } + +SnAbuseReport _$SnAbuseReportFromJson(Map 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 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 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 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 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 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; +} diff --git a/lib/types/account.g.dart b/lib/types/account.g.dart index c9c491e..3e91de3 100644 --- a/lib/types/account.g.dart +++ b/lib/types/account.g.dart @@ -212,3 +212,29 @@ Map _$$SnAccountStatusInfoImplToJson( 'last_seen_at': instance.lastSeenAt?.toIso8601String(), 'status': instance.status, }; + +_$SnAbuseReportImpl _$$SnAbuseReportImplFromJson(Map 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 _$$SnAbuseReportImplToJson(_$SnAbuseReportImpl instance) => + { + '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, + };