diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 28e8b49..bff96e5 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -432,5 +432,9 @@ "serviceStatus": "Service Status", "termRelated": "Related Terms", "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." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index c152327..413cd48 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -430,5 +430,9 @@ "serviceStatus": "服务状态", "termRelated": "相关条款", "appDetails": "应用程序详情", - "postRecommendation": "推荐帖子" + "postRecommendation": "推荐帖子", + "publisherBlockHint": "屏蔽 {}", + "publisherBlockHintDescription": "你正要屏蔽此发布者的运营者,该操作也将屏蔽由同一用户运营的发布者。", + "userUnblocked": "已解除屏蔽用户 {}", + "userBlocked": "已屏蔽用户 {}" } diff --git a/lib/providers/relationship.dart b/lib/providers/relationship.dart index c033f1e..9198b7a 100644 --- a/lib/providers/relationship.dart +++ b/lib/providers/relationship.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/account.dart'; class SnRelationshipProvider { late final SnNetworkProvider _sn; @@ -9,6 +10,15 @@ class SnRelationshipProvider { _sn = context.read(); } + Future 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 updateRelationship( int relatedId, int status, diff --git a/lib/screens/abuse_report.dart b/lib/screens/abuse_report.dart index ce8188b..078a617 100644 --- a/lib/screens/abuse_report.dart +++ b/lib/screens/abuse_report.dart @@ -39,7 +39,7 @@ class _AbuseReportScreenState extends State { void _showAbuseReportDialog() { showDialog( context: context, - builder: (context) => _AbuseReportDialog(), + builder: (context) => AbuseReportDialog(), ).then((value) { if (value == true && mounted) { _fetchReports(); @@ -91,19 +91,29 @@ class _AbuseReportScreenState extends State { } } -class _AbuseReportDialog extends StatefulWidget { - const _AbuseReportDialog({super.key}); +class AbuseReportDialog extends StatefulWidget { + final String? resourceLocation; + + const AbuseReportDialog({super.key, this.resourceLocation}); @override - State<_AbuseReportDialog> createState() => _AbuseReportDialogState(); + State createState() => _AbuseReportDialogState(); } -class _AbuseReportDialogState extends State<_AbuseReportDialog> { +class _AbuseReportDialogState extends State { bool _isBusy = false; final _resourceController = TextEditingController(); final _reasonController = TextEditingController(); + @override + void initState() { + super.initState(); + if (widget.resourceLocation != null) { + _resourceController.text = widget.resourceLocation!; + } + } + @override dispose() { _resourceController.dispose(); @@ -144,6 +154,7 @@ class _AbuseReportDialogState extends State<_AbuseReportDialog> { const Gap(12), TextField( controller: _resourceController, + readOnly: widget.resourceLocation != null, maxLength: null, decoration: InputDecoration( border: const UnderlineInputBorder(), diff --git a/lib/screens/post/publisher_page.dart b/lib/screens/post/publisher_page.dart index f2a7191..ce8600e 100644 --- a/lib/screens/post/publisher_page.dart +++ b/lib/screens/post/publisher_page.dart @@ -21,6 +21,9 @@ import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/universal_image.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; +import '../../providers/relationship.dart'; +import '../abuse_report.dart'; + class PostPublisherScreen extends StatefulWidget { final String name; @@ -35,18 +38,21 @@ class _PostPublisherScreenState extends State with SingleTi late final TabController _tabController = TabController(length: 3, vsync: this); SnPublisher? _publisher; - SnRealm? _realm; SnAccount? _account; + SnRelationship? _accountRelationship; + SnRealm? _realm; Future _fetchPublisher() async { try { final sn = context.read(); final ud = context.read(); + final rel = context.read(); final resp = await sn.client.get('/cgi/co/publishers/${widget.name}'); if (!mounted) return; _publisher = SnPublisher.fromJson(resp.data); _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}'); _realm = SnRealm.fromJson(resp.data); } @@ -160,11 +166,73 @@ class _PostPublisherScreenState extends State with SingleTi } } + bool _isWorking = false; + + Future _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(); + 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 _unblockPublisher() async { + if (_isWorking) return; + + setState(() => _isWorking = true); + + try { + final sn = context.read(); + final rel = context.read(); + 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() { _posts.clear(); _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 void initState() { super.initState(); @@ -319,8 +387,48 @@ class _PostPublisherScreenState extends State with SingleTi label: Text('unsubscribe').tr(), 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), Text(_publisher!.description).padding(horizontal: 8), const Gap(12), diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index 8368410..c8bc85d 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -149,7 +149,7 @@ class _RealmDetailHomeWidget extends StatelessWidget { itemBuilder: (context, idx) { final ele = publishers![idx]; return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24), + contentPadding: const EdgeInsets.symmetric(horizontal: 20), leading: AccountImage( content: ele.avatar, fallbackWidget: const Icon(Symbols.group, size: 24), diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart index c5fdb2d..2379c91 100644 --- a/lib/widgets/dialog.dart +++ b/lib/widgets/dialog.dart @@ -99,7 +99,7 @@ extension AppPromptExtension on BuildContext { if (exception.response != null) { 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 { content = Text(preview);