Allow user blocking publisher's user and report it

This commit is contained in:
LittleSheep 2024-12-11 23:53:03 +08:00
parent e05209ba3c
commit 811fc40d79
7 changed files with 149 additions and 12 deletions

View File

@ -432,5 +432,9 @@
"serviceStatus": "Service Status", "serviceStatus": "Service Status",
"termRelated": "Related Terms", "termRelated": "Related Terms",
"appDetails": "App Details", "appDetails": "App Details",
"postRecommendation": "Highlight Posts" "postRecommendation": "Highlight Posts",
"publisherBlockHint": "Block {}",
"publisherBlockHintDescription": "You are going to block this publisher's maintainer, this will also block publishers that run by the same user.",
"userUnblocked": "{} has been unblocked.",
"userBlocked": "{} has been blocked."
} }

View File

@ -430,5 +430,9 @@
"serviceStatus": "服务状态", "serviceStatus": "服务状态",
"termRelated": "相关条款", "termRelated": "相关条款",
"appDetails": "应用程序详情", "appDetails": "应用程序详情",
"postRecommendation": "推荐帖子" "postRecommendation": "推荐帖子",
"publisherBlockHint": "屏蔽 {}",
"publisherBlockHintDescription": "你正要屏蔽此发布者的运营者,该操作也将屏蔽由同一用户运营的发布者。",
"userUnblocked": "已解除屏蔽用户 {}",
"userBlocked": "已屏蔽用户 {}"
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart';
class SnRelationshipProvider { class SnRelationshipProvider {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
@ -9,6 +10,15 @@ class SnRelationshipProvider {
_sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
} }
Future<SnRelationship?> getRelationship(int relatedId) async {
try {
final resp = await _sn.client.get('/cgi/id/users/me/relations/$relatedId');
return SnRelationship.fromJson(resp.data);
} catch (err) {
return null;
}
}
Future<void> updateRelationship( Future<void> updateRelationship(
int relatedId, int relatedId,
int status, int status,

View File

@ -39,7 +39,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
void _showAbuseReportDialog() { void _showAbuseReportDialog() {
showDialog( showDialog(
context: context, context: context,
builder: (context) => _AbuseReportDialog(), builder: (context) => AbuseReportDialog(),
).then((value) { ).then((value) {
if (value == true && mounted) { if (value == true && mounted) {
_fetchReports(); _fetchReports();
@ -91,19 +91,29 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
} }
} }
class _AbuseReportDialog extends StatefulWidget { class AbuseReportDialog extends StatefulWidget {
const _AbuseReportDialog({super.key}); final String? resourceLocation;
const AbuseReportDialog({super.key, this.resourceLocation});
@override @override
State<_AbuseReportDialog> createState() => _AbuseReportDialogState(); State<AbuseReportDialog> createState() => _AbuseReportDialogState();
} }
class _AbuseReportDialogState extends State<_AbuseReportDialog> { class _AbuseReportDialogState extends State<AbuseReportDialog> {
bool _isBusy = false; bool _isBusy = false;
final _resourceController = TextEditingController(); final _resourceController = TextEditingController();
final _reasonController = TextEditingController(); final _reasonController = TextEditingController();
@override
void initState() {
super.initState();
if (widget.resourceLocation != null) {
_resourceController.text = widget.resourceLocation!;
}
}
@override @override
dispose() { dispose() {
_resourceController.dispose(); _resourceController.dispose();
@ -144,6 +154,7 @@ class _AbuseReportDialogState extends State<_AbuseReportDialog> {
const Gap(12), const Gap(12),
TextField( TextField(
controller: _resourceController, controller: _resourceController,
readOnly: widget.resourceLocation != null,
maxLength: null, maxLength: null,
decoration: InputDecoration( decoration: InputDecoration(
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),

View File

@ -21,6 +21,9 @@ import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../providers/relationship.dart';
import '../abuse_report.dart';
class PostPublisherScreen extends StatefulWidget { class PostPublisherScreen extends StatefulWidget {
final String name; final String name;
@ -35,18 +38,21 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
late final TabController _tabController = TabController(length: 3, vsync: this); late final TabController _tabController = TabController(length: 3, vsync: this);
SnPublisher? _publisher; SnPublisher? _publisher;
SnRealm? _realm;
SnAccount? _account; SnAccount? _account;
SnRelationship? _accountRelationship;
SnRealm? _realm;
Future<void> _fetchPublisher() async { Future<void> _fetchPublisher() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
final rel = context.read<SnRelationshipProvider>();
final resp = await sn.client.get('/cgi/co/publishers/${widget.name}'); final resp = await sn.client.get('/cgi/co/publishers/${widget.name}');
if (!mounted) return; if (!mounted) return;
_publisher = SnPublisher.fromJson(resp.data); _publisher = SnPublisher.fromJson(resp.data);
_account = await ud.getAccount(_publisher?.accountId); _account = await ud.getAccount(_publisher?.accountId);
if (_publisher?.realmId != null) { _accountRelationship = await rel.getRelationship(_account!.id);
if (_publisher?.realmId != null && _publisher!.realmId != 0) {
final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}'); final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
_realm = SnRealm.fromJson(resp.data); _realm = SnRealm.fromJson(resp.data);
} }
@ -160,11 +166,73 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
} }
} }
bool _isWorking = false;
Future<void> _blockPublisher() async {
if (_isWorking) return;
final confirm = await context.showConfirmDialog(
'publisherBlockHint'.tr(args: ['@${_publisher?.name ?? 'unknown'.tr()}']),
'publisherBlockHintDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
setState(() => _isWorking = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/users/me/relations/block', data: {
'related': _account!.name,
});
if (!mounted) return;
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isWorking = false);
}
}
Future<void> _unblockPublisher() async {
if (_isWorking) return;
setState(() => _isWorking = true);
try {
final sn = context.read<SnNetworkProvider>();
final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
if (!mounted) return;
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isWorking = false);
}
}
void _updateFetchType() { void _updateFetchType() {
_posts.clear(); _posts.clear();
_fetchPosts(); _fetchPosts();
} }
void _showAbuseReportDialog() {
showDialog(
context: context,
builder: (context) => AbuseReportDialog(
resourceLocation: 'pub:${_publisher?.name}',
),
).then((value) {
if (value == true && mounted) {
_fetchPosts();
context.showSnackbar('abuseReportSubmitted'.tr());
}
});
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -319,8 +387,48 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
label: Text('unsubscribe').tr(), label: Text('unsubscribe').tr(),
icon: const Icon(Symbols.remove), icon: const Icon(Symbols.remove),
), ),
PopupMenuButton(
padding: EdgeInsets.zero,
style: ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.flag),
const Gap(16),
Text('report').tr(),
],
),
onTap: () => _showAbuseReportDialog(),
),
if (_accountRelationship?.status != 2)
PopupMenuItem(
onTap: _blockPublisher,
child: Row(
children: [
const Icon(Symbols.block),
const Gap(16),
Text('friendBlock').tr(),
],
),
)
else
PopupMenuItem(
onTap: _unblockPublisher,
child: Row(
children: [
const Icon(Symbols.block),
const Gap(16),
Text('friendUnblock').tr(),
],
),
),
],
),
], ],
).padding(right: 8), ),
const Gap(12), const Gap(12),
Text(_publisher!.description).padding(horizontal: 8), Text(_publisher!.description).padding(horizontal: 8),
const Gap(12), const Gap(12),

View File

@ -149,7 +149,7 @@ class _RealmDetailHomeWidget extends StatelessWidget {
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final ele = publishers![idx]; final ele = publishers![idx];
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: AccountImage( leading: AccountImage(
content: ele.avatar, content: ele.avatar,
fallbackWidget: const Icon(Symbols.group, size: 24), fallbackWidget: const Icon(Symbols.group, size: 24),

View File

@ -99,7 +99,7 @@ extension AppPromptExtension on BuildContext {
if (exception.response != null) { if (exception.response != null) {
content = Text( content = Text(
'$preview\n\n(${exception.response?.statusCode}) ${exception.response?.data}', '$preview\n\n${exception.requestOptions.uri.path}\n(${exception.response?.statusCode}) ${exception.response?.data}',
); );
} else { } else {
content = Text(preview); content = Text(preview);