diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b3f102e2..4ec79754 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -204,14 +204,14 @@ "ownerPanel": "Owner Panel", "reportStatus_any": "All", "reportStatus_appeal": "Appeal", - "reportStatus_approved": "Approved", "reportStatus_closed": "Closed", "reportStatus_pending": "Pending", - "reportStatus_rejected": "Rejected", - "report_accept": "Accept", - "report_reject": "Reject", + "reportStatus_deleted": "Deleted", + "reportStatus_restored": "Restored", "reportedUser": "Reported user: ", "reportedBy": "Reported by: ", + "reportDeleteMessage": "Delete the reported post.", + "reportRestoreMessage": "Resolve or restore the reported post.", "copy": "Copy", "copied": "Copied", "share": "Share", @@ -457,6 +457,7 @@ } }, "edit": "Edit", + "restore": "Restore", "delete": "Delete", "deleteX": "Delete {name}", "@deleteX": { @@ -494,6 +495,8 @@ "reason": "Reason", "moderate": "Moderate", "submit": "Submit", + "resolve": "Resolve", + "unresolve": "Unresolve", "alternativeSources": "Alternative Sources", "markdownEditor_heading": "Heading", "markdownEditor_bold": "Bold", diff --git a/lib/src/api/community.dart b/lib/src/api/community.dart index 737d3f52..1d8622cc 100644 --- a/lib/src/api/community.dart +++ b/lib/src/api/community.dart @@ -233,9 +233,7 @@ class APICommunity { final response = await client.get(path, queryParams: query); - return DetailedCommunityModel.fromLemmy( - response.bodyJson['community_view']! as JsonMap, - ); + return DetailedCommunityModel.fromLemmy(response.bodyJson); case ServerSoftware.piefed: const path = '/community'; @@ -262,9 +260,7 @@ class APICommunity { final response = await client.get(path, queryParams: query); - return DetailedCommunityModel.fromLemmy( - response.bodyJson['community_view']! as JsonMap, - ); + return DetailedCommunityModel.fromLemmy(response.bodyJson); case ServerSoftware.piefed: const path = '/community'; diff --git a/lib/src/api/community_moderation.dart b/lib/src/api/community_moderation.dart index 72bf6f72..328df7a5 100644 --- a/lib/src/api/community_moderation.dart +++ b/lib/src/api/community_moderation.dart @@ -1,6 +1,8 @@ import 'package:interstellar/src/api/client.dart'; import 'package:interstellar/src/controller/server.dart'; import 'package:interstellar/src/models/community.dart'; +import 'package:interstellar/src/models/user.dart'; +import 'package:interstellar/src/utils/models.dart'; import 'package:interstellar/src/utils/utils.dart'; enum ReportStatus { any, approved, pending, rejected } @@ -24,6 +26,25 @@ class APICommunityModeration { return CommunityReportListModel.fromMbin(response.bodyJson); case ServerSoftware.lemmy: + const path = '/post/report/list'; + final query = { + 'page': page, + 'community_id': communityId.toString(), + 'unresolved_only': (status == ReportStatus.pending).toString(), + }; + + final response = await client.get(path, queryParams: query); + + final json = response.bodyJson; + json['next_page'] = lemmyCalcNextIntPage( + json['post_reports']! as List, + page, + ); + + return CommunityReportListModel.fromLemmy( + json, + langCodeIdPairs: await client.languageCodeIdPairs(), + ); case ServerSoftware.piefed: throw UnimplementedError('Not yet implemented'); } @@ -32,6 +53,7 @@ class APICommunityModeration { Future acceptReport( int communityId, int reportId, + int postId, ) async { switch (client.software) { case ServerSoftware.mbin: @@ -41,6 +63,27 @@ class APICommunityModeration { return CommunityReportModel.fromMbin(response.bodyJson); case ServerSoftware.lemmy: + { + const path = '/post/remove'; + + final response = await client.post( + path, + body: {'post_id': postId, 'removed': true, 'reason': 'Moderated'}, + ); + } + + const path = '/post/report/resolve'; + + final response = await client.put( + path, + body: {'report_id': reportId, 'resolved': true}, + ); + + return CommunityReportModel.fromLemmy( + response.bodyJson['post_report_view']! as JsonMap, + langCodeIdPairs: await client.languageCodeIdPairs(), + ); + case ServerSoftware.piefed: throw UnimplementedError('Not yet implemented'); } @@ -49,6 +92,7 @@ class APICommunityModeration { Future rejectReport( int communityId, int reportId, + int postId, ) async { switch (client.software) { case ServerSoftware.mbin: @@ -58,6 +102,27 @@ class APICommunityModeration { return CommunityReportModel.fromMbin(response.bodyJson); case ServerSoftware.lemmy: + { + const path = '/post/remove'; + + final response = await client.post( + path, + body: {'post_id': postId, 'removed': false, 'reason': 'Moderated'}, + ); + } + + const path = '/post/report/resolve'; + + final response = await client.put( + path, + body: {'report_id': reportId, 'resolved': true}, + ); + + return CommunityReportModel.fromLemmy( + response.bodyJson['post_report_view']! as JsonMap, + langCodeIdPairs: await client.languageCodeIdPairs(), + ); + case ServerSoftware.piefed: throw UnimplementedError('Not yet implemented'); } @@ -108,7 +173,20 @@ class APICommunityModeration { return CommunityBanModel.fromMbin(response.bodyJson); case ServerSoftware.lemmy: - throw Exception('Ban update not implemented on Lemmy yet'); + const path = '/community/ban_user'; + + final response = await client.post( + path, + body: { + 'community_id': communityId, + 'person_id': userId, + 'ban': true, + 'reason': reason, + 'expires_at': ?expiredAt?.microsecondsSinceEpoch, + }, + ); + + return CommunityBanModel.fromLemmy(response.bodyJson); case ServerSoftware.piefed: const path = '/community/moderate/ban'; @@ -116,7 +194,7 @@ class APICommunityModeration { 'community_id': communityId, 'user_id': userId, 'reason': reason, - if (expiredAt != null) 'expires_at': expiredAt.toIso8601String(), + 'expires_at': ?expiredAt?.toIso8601String(), }; final response = await client.post(path, body: body); @@ -135,7 +213,18 @@ class APICommunityModeration { return CommunityBanModel.fromMbin(response.bodyJson); case ServerSoftware.lemmy: - throw Exception('Ban update not implemented on Lemmy yet'); + const path = '/community/ban_user'; + + final response = await client.post( + path, + body: { + 'community_id': communityId, + 'person_id': userId, + 'ban': false, + }, + ); + + return CommunityBanModel.fromLemmy(response.bodyJson); case ServerSoftware.piefed: const path = '/community/moderate/unban'; @@ -232,7 +321,20 @@ class APICommunityModeration { return DetailedCommunityModel.fromMbin(response.bodyJson); case ServerSoftware.lemmy: - throw Exception('Community edit not implemented on Lemmy yet'); + const path = '/community'; + + final response = await client.put( + path, + body: { + 'community_id': communityId, + 'title': title, + 'description': description, + 'nsfw': isAdult, + 'posting_restricted_to_mods': isPostingRestrictedToMods, + }, + ); + + return DetailedCommunityModel.fromLemmy(response.bodyJson); case ServerSoftware.piefed: const path = '/community'; @@ -252,7 +354,7 @@ class APICommunityModeration { } } - Future updateModerator( + Future> updateModerator( int communityId, int userId, bool state, @@ -265,10 +367,23 @@ class APICommunityModeration { ? await client.post(path) : await client.delete(path); - return DetailedCommunityModel.fromMbin(response.bodyJson); + return DetailedCommunityModel.fromMbin(response.bodyJson).moderators; case ServerSoftware.lemmy: - throw Exception('Moderator update not implemented on Lemmy yet'); + const path = '/community/mod'; + + final response = await client.post( + path, + body: { + 'community_id': communityId, + 'person_id': userId, + 'added': state, + }, + ); + + return (response.bodyJson['moderators']! as List) + .map((moderator) => UserModel.fromLemmy(moderator['moderator'])) + .toList(); case ServerSoftware.piefed: throw UnimplementedError(); diff --git a/lib/src/api/moderation.dart b/lib/src/api/moderation.dart index b645e251..7c99acda 100644 --- a/lib/src/api/moderation.dart +++ b/lib/src/api/moderation.dart @@ -116,7 +116,21 @@ class APIModeration { }; case ServerSoftware.lemmy: - throw Exception('Moderation not implemented on Lemmy yet'); + const path = '/post/feature'; + + final response = await client.post( + path, + body: { + 'post_id': postId, + 'featured': pinned, + 'feature_type': 'Community', + }, + ); + + return PostModel.fromLemmy( + response.bodyJson, + langCodeIdPairs: await client.languageCodeIdPairs(), + ); case ServerSoftware.piefed: const path = '/post/feature'; @@ -188,7 +202,17 @@ class APIModeration { }; case ServerSoftware.lemmy: - throw Exception('Moderation not implemented on Lemmy yet'); + const path = '/post/remove'; + + final response = await client.post( + path, + body: {'post_id': postId, 'removed': status, 'reason': 'Moderated'}, + ); + + return PostModel.fromLemmy( + response.bodyJson, + langCodeIdPairs: await client.languageCodeIdPairs(), + ); case ServerSoftware.piefed: const path = '/post/remove'; @@ -287,7 +311,21 @@ class APIModeration { return CommentModel.fromMbin(response.bodyJson); case ServerSoftware.lemmy: - throw Exception('Moderation not implemented on Lemmy yet'); + const path = '/comment/remove'; + + final response = await client.post( + path, + body: { + 'comment_id': commentId, + 'removed': status, + 'reason': 'Moderated', + }, + ); + + return CommentModel.fromLemmy( + response.bodyJson['comment_view']! as JsonMap, + langCodeIdPairs: await client.languageCodeIdPairs(), + ); case ServerSoftware.piefed: const path = '/comment/remove'; diff --git a/lib/src/models/comment.dart b/lib/src/models/comment.dart index f99b8077..2a35a918 100644 --- a/lib/src/models/comment.dart +++ b/lib/src/models/comment.dart @@ -131,7 +131,7 @@ abstract class CommentModel with _$CommentModel { required DateTime? editedAt, required List? children, required int childCount, - required String visibility, + required PostVisibility visibility, required bool? canAuthUserModerate, required NotificationControlStatus? notificationControlStatus, required List? bookmarks, @@ -164,7 +164,7 @@ abstract class CommentModel with _$CommentModel { .map((c) => CommentModel.fromMbin(c as JsonMap)) .toList(), childCount: json['childCount']! as int, - visibility: json['visibility']! as String, + visibility: PostVisibility.values.byName(json['visibility']! as String), canAuthUserModerate: json['canAuthUserModerate'] as bool?, notificationControlStatus: null, bookmarks: optionalStringList(json['bookmarks']), @@ -229,7 +229,11 @@ abstract class CommentModel with _$CommentModel { editedAt: optionalDateTime(json['updated'] as String?), children: children, childCount: lemmyCounts?['child_count'] as int? ?? 0, - visibility: 'visible', + visibility: (lemmyComment['deleted']! as bool) + ? PostVisibility.soft_deleted + : (lemmyComment['removed']! as bool) + ? PostVisibility.trashed + : PostVisibility.visible, canAuthUserModerate: null, notificationControlStatus: null, bookmarks: [ @@ -319,7 +323,11 @@ abstract class CommentModel with _$CommentModel { editedAt: optionalDateTime(json['updated'] as String?), children: children, childCount: piefedCounts?['child_count'] as int? ?? 0, - visibility: 'visible', + visibility: (piefedComment['deleted']! as bool) + ? PostVisibility.soft_deleted + : (piefedComment['removed']! as bool) + ? PostVisibility.trashed + : PostVisibility.visible, canAuthUserModerate: json['can_auth_user_moderate'] as bool?, notificationControlStatus: json['activity_alert'] == null ? null diff --git a/lib/src/models/community.dart b/lib/src/models/community.dart index 3ee8d17d..14344978 100644 --- a/lib/src/models/community.dart +++ b/lib/src/models/community.dart @@ -106,8 +106,16 @@ abstract class DetailedCommunityModel with _$DetailedCommunityModel { } factory DetailedCommunityModel.fromLemmy(JsonMap json) { - final lemmyCommunity = json['community']! as JsonMap; - final lemmyCounts = json['counts']! as JsonMap; + final communityView = json['community_view'] as JsonMap? ?? json; + final lemmyCommunity = communityView['community']! as JsonMap; + final lemmyCounts = communityView['counts']! as JsonMap; + + final mods = ((json['moderators'] ?? []) as List) + .map( + (user) => + UserModel.fromLemmy((user as JsonMap)['moderator']! as JsonMap), + ) + .toList(); final community = DetailedCommunityModel( id: lemmyCommunity['id']! as int, @@ -115,16 +123,17 @@ abstract class DetailedCommunityModel with _$DetailedCommunityModel { title: lemmyCommunity['title']! as String, icon: lemmyGetOptionalImage(lemmyCommunity['icon'] as String?), description: lemmyCommunity['description'] as String?, - owner: null, - moderators: [], + owner: mods.firstOrNull, + moderators: mods, subscriptionsCount: lemmyCounts['subscribers']! as int, threadCount: lemmyCounts['posts']! as int, threadCommentCount: lemmyCounts['comments']! as int, microblogCount: null, microblogCommentCount: null, isAdult: lemmyCommunity['nsfw']! as bool, - isUserSubscribed: (json['subscribed']! as String) != 'NotSubscribed', - isBlockedByUser: json['blocked'] as bool?, + isUserSubscribed: + (communityView['subscribed']! as String) != 'NotSubscribed', + isBlockedByUser: communityView['blocked'] as bool?, isPostingRestrictedToMods: lemmyCommunity['posting_restricted_to_mods']! as bool, notificationControlStatus: null, @@ -142,27 +151,21 @@ abstract class DetailedCommunityModel with _$DetailedCommunityModel { final piefedCommunity = communityView['community']! as JsonMap; final piefedCounts = communityView['counts']! as JsonMap; + final mods = ((json['moderators'] ?? []) as List) + .map( + (user) => + UserModel.fromPiefed((user as JsonMap)['moderator']! as JsonMap), + ) + .toList(); + final community = DetailedCommunityModel( id: piefedCommunity['id']! as int, name: getLemmyPiefedActorName(piefedCommunity), title: piefedCommunity['title']! as String, icon: lemmyGetOptionalImage(piefedCommunity['icon'] as String?), description: piefedCommunity['description'] as String?, - owner: ((json['moderators'] ?? []) as List) - .map( - (user) => UserModel.fromPiefed( - (user as JsonMap)['moderator']! as JsonMap, - ), - ) - .toList() - .firstOrNull, - moderators: ((json['moderators'] ?? []) as List) - .map( - (user) => UserModel.fromPiefed( - (user as JsonMap)['moderator']! as JsonMap, - ), - ) - .toList(), + owner: mods.firstOrNull, + moderators: mods, subscriptionsCount: piefedCounts['subscriptions_count']! as int, threadCount: piefedCounts['post_count']! as int, threadCommentCount: piefedCounts['post_reply_count']! as int, @@ -325,6 +328,21 @@ abstract class CommunityReportListModel with _$CommunityReportListModel { .toList(), nextPage: mbinCalcNextPaginationPage(json['pagination']! as JsonMap), ); + + factory CommunityReportListModel.fromLemmy( + JsonMap json, { + required List<(String, int)> langCodeIdPairs, + }) => CommunityReportListModel( + items: (json['post_reports']! as List) + .map( + (item) => CommunityReportModel.fromLemmy( + item as JsonMap, + langCodeIdPairs: langCodeIdPairs, + ), + ) + .toList(), + nextPage: json['next_page'] as String?, + ); } @freezed @@ -383,4 +401,50 @@ abstract class CommunityReportModel with _$CommunityReportModel { weight: json['weight'] as int?, ); } + + factory CommunityReportModel.fromLemmy( + JsonMap json, { + required List<(String, int)> langCodeIdPairs, + }) { + final type = json['type'] as String?; + final report = json['post_report']! as JsonMap; + + final subjectPost = json['post'] == null + ? null + : PostModel.fromLemmy({ + 'post_view': { + 'post': json['post']! as JsonMap, + 'community': json['community']! as JsonMap, + 'creator': json['post_creator']! as JsonMap, + }, + }, langCodeIdPairs: langCodeIdPairs); + final subjectComment = json['comment'] == null + ? null + : CommentModel.fromLemmy( + json['comment']! as JsonMap, + langCodeIdPairs: langCodeIdPairs, + ); + + return CommunityReportModel( + id: report['id']! as int, + community: CommunityModel.fromLemmy(json['community']! as JsonMap), + reportedBy: UserModel.fromLemmy(json['creator']! as JsonMap), + reportedUser: UserModel.fromLemmy(json['post_creator']! as JsonMap), + subjectPost: subjectPost, + subjectComment: subjectComment, + reason: report['reason']! as String, + status: !(report['resolved']! as bool) + ? ReportStatus.pending + : (subjectPost?.visibility == PostVisibility.trashed || + subjectComment?.visibility == PostVisibility.trashed) + ? ReportStatus.approved + : ReportStatus.rejected, + createdAt: optionalDateTime(report['published'] as String?), + consideredAt: null, + consideredBy: json['resolver'] != null + ? UserModel.fromLemmy(json['resolver']! as JsonMap) + : null, + weight: null, + ); + } } diff --git a/lib/src/models/post.dart b/lib/src/models/post.dart index 045a99aa..3048884d 100644 --- a/lib/src/models/post.dart +++ b/lib/src/models/post.dart @@ -16,6 +16,8 @@ part 'post.freezed.dart'; enum PostType { thread, microblog } +enum PostVisibility { visible, trashed, soft_deleted, private } + @freezed abstract class PostListModel with _$PostListModel { const factory PostListModel({ @@ -96,7 +98,7 @@ abstract class PostModel with _$PostModel { required DateTime createdAt, required DateTime? editedAt, required DateTime lastActive, - required String visibility, + required PostVisibility visibility, required bool? canAuthUserModerate, required NotificationControlStatus? notificationControlStatus, required List? bookmarks, @@ -140,7 +142,7 @@ abstract class PostModel with _$PostModel { createdAt: DateTime.parse(json['createdAt']! as String), editedAt: optionalDateTime(json['editedAt'] as String?), lastActive: DateTime.parse(json['lastActive']! as String), - visibility: json['visibility']! as String, + visibility: PostVisibility.values.byName(json['visibility']! as String), canAuthUserModerate: json['canAuthUserModerate'] as bool?, notificationControlStatus: json['notificationStatus'] == null ? null @@ -187,7 +189,7 @@ abstract class PostModel with _$PostModel { createdAt: DateTime.parse(json['createdAt']! as String), editedAt: optionalDateTime(json['editedAt'] as String?), lastActive: DateTime.parse(json['lastActive']! as String), - visibility: json['visibility']! as String, + visibility: PostVisibility.values.byName(json['visibility']! as String), canAuthUserModerate: json['canAuthUserModerate'] as bool?, notificationControlStatus: json['notificationStatus'] == null ? null @@ -261,7 +263,11 @@ abstract class PostModel with _$PostModel { lastActive: lemmyCounts == null ? DateTime.now() : DateTime.parse(lemmyCounts['newest_comment_time']! as String), - visibility: 'visible', + visibility: (lemmyPost['deleted']! as bool) + ? PostVisibility.soft_deleted + : (lemmyPost['removed']! as bool) + ? PostVisibility.trashed + : PostVisibility.visible, canAuthUserModerate: null, notificationControlStatus: null, bookmarks: [ @@ -345,7 +351,7 @@ abstract class PostModel with _$PostModel { lastActive: DateTime.parse( piefedCounts['newest_comment_time']! as String, ), - visibility: 'visible', + visibility: PostVisibility.visible, canAuthUserModerate: postView['can_auth_user_moderate'] as bool?, notificationControlStatus: postView['activity_alert'] == null ? null diff --git a/lib/src/screens/explore/community_mod_panel.dart b/lib/src/screens/explore/community_mod_panel.dart index 14f4f647..c9cd30d2 100644 --- a/lib/src/screens/explore/community_mod_panel.dart +++ b/lib/src/screens/explore/community_mod_panel.dart @@ -5,10 +5,13 @@ import 'package:interstellar/src/controller/controller.dart'; import 'package:interstellar/src/controller/router.gr.dart'; import 'package:interstellar/src/controller/server.dart'; import 'package:interstellar/src/models/community.dart'; +import 'package:interstellar/src/models/post.dart'; import 'package:interstellar/src/screens/explore/user_item.dart'; import 'package:interstellar/src/screens/feed/post_page.dart'; import 'package:interstellar/src/utils/breakpoints.dart'; import 'package:interstellar/src/utils/utils.dart'; +import 'package:interstellar/src/widgets/ban_dialog.dart'; +import 'package:interstellar/src/widgets/context_menu.dart'; import 'package:interstellar/src/widgets/display_name.dart'; import 'package:interstellar/src/widgets/loading_button.dart'; import 'package:interstellar/src/widgets/paging.dart'; @@ -46,6 +49,8 @@ class _CommunityModPanelScreenState extends State { @override Widget build(BuildContext context) { + final ac = context.read(); + void onUpdate(DetailedCommunityModel newValue) { setState(() { _data = newValue; @@ -54,18 +59,15 @@ class _CommunityModPanelScreenState extends State { } return DefaultTabController( - length: - context.read().serverSoftware == ServerSoftware.mbin - ? 2 - : 1, + length: ac.serverSoftware == ServerSoftware.mbin ? 2 : 1, child: Scaffold( appBar: AppBar( title: Text('Mod Panel for ${widget.initData.name}'), bottom: TabBar( tabs: [ - const Tab(text: 'Bans'), - if (context.read().serverSoftware == - ServerSoftware.mbin) + if (ac.serverSoftware != ServerSoftware.lemmy) + const Tab(text: 'Bans'), + if (ac.serverSoftware != ServerSoftware.piefed) const Tab(text: 'Reports'), ], ), @@ -73,9 +75,9 @@ class _CommunityModPanelScreenState extends State { body: TabBarView( physics: appTabViewPhysics(context), children: [ - CommunityModPanelBans(data: _data, onUpdate: onUpdate), - if (context.read().serverSoftware == - ServerSoftware.mbin) + if (ac.serverSoftware != ServerSoftware.lemmy) + CommunityModPanelBans(data: _data, onUpdate: onUpdate), + if (ac.serverSoftware != ServerSoftware.piefed) CommunityModPanelReports(data: _data, onUpdate: onUpdate), ], ), @@ -221,133 +223,251 @@ class _MagazineModPanelReportsState extends State { ), ], itemBuilder: (context, item, index) { - return InkWell( - onTap: () { - if (item.subjectPost != null) { - pushPostPage( - context, - communityName: item.subjectPost!.community.name, - postId: item.subjectPost!.id, - postType: item.subjectPost!.type, - initData: item.subjectPost, - userCanModerate: true, - ); - } else if (item.subjectComment != null) { - context.router.push( - PostCommentRoute( - postType: item.subjectComment!.postType, - commentId: item.subjectComment!.id, - ), - ); + return CommunityModReport( + communityId: widget.data.id, + report: item, + updateItem: (newItem) { + if (newItem == null) { + _pagingController.removeItem(item); + } else { + _pagingController.updateItem(item, newItem); } }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + }, + ); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } +} + +class CommunityModReport extends StatefulWidget { + const CommunityModReport({ + required this.communityId, + required this.report, + required this.updateItem, + super.key, + }); + + final int communityId; + final CommunityReportModel report; + final void Function(CommunityReportModel? newItem) updateItem; + + @override + State createState() => _CommunityModReportState(); +} + +class _CommunityModReportState extends State { + bool _deleted = false; + + @override + void initState() { + super.initState(); + + _deleted = + widget.report.subjectPost?.visibility == PostVisibility.trashed || + widget.report.subjectPost?.visibility == PostVisibility.soft_deleted; + } + + void _modMenu(BuildContext context) { + final ac = context.read(); + ContextMenu( + items: [ + ContextMenuItem( + title: l(context).pin, + onTap: () async => ac.api.moderation.postPin( + widget.report.subjectPost!.type, + widget.report.subjectPost!.id, + !widget.report.subjectPost!.isPinned, + ), + ), + ContextMenuItem( + title: l(context).notSafeForWork_mark, + onTap: () async => ac.api.moderation.postMarkNSFW( + widget.report.subjectPost!.type, + widget.report.subjectPost!.id, + !widget.report.subjectPost!.isNSFW, + ), + ), + ContextMenuItem( + title: l(context).delete, + onTap: () async => ac.api.moderation.postDelete( + widget.report.subjectPost!.type, + widget.report.subjectPost!.id, + !_deleted, + ), + ), + ContextMenuItem( + title: l(context).banUser, + onTap: () async => openBanDialog( + context, + user: widget.report.subjectPost!.user, + community: widget.report.subjectPost!.community, + ), + ), + ContextMenuItem( + title: l(context).lock, + onTap: () async => ac.api.moderation.postLock( + widget.report.subjectPost!.type, + widget.report.subjectPost!.id, + !widget.report.subjectPost!.isLocked, + ), + ), + ], + ).openMenu(context); + } + + @override + Widget build(BuildContext context) { + final ac = context.read(); + + return InkWell( + onTap: () { + if (widget.report.subjectPost != null) { + pushPostPage( + context, + communityName: widget.report.subjectPost!.community.name, + postId: widget.report.subjectPost!.id, + postType: widget.report.subjectPost!.type, + initData: widget.report.subjectPost, + userCanModerate: true, + ); + } else if (widget.report.subjectComment != null) { + context.router.push( + PostCommentRoute( + postType: widget.report.subjectComment!.postType, + commentId: widget.report.subjectComment!.id, + ), + ); + } + }, + onLongPress: widget.report.subjectPost == null + ? null + : () => _modMenu(context), + onSecondaryTap: widget.report.subjectPost == null + ? null + : () => _modMenu(context), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - Text(l(context).reportedBy), - DisplayName( - item.reportedBy!.name, - icon: item.reportedBy!.avatar, - onTap: () => context.router.push( - UserRoute( - username: item.reportedBy!.name, - userId: item.reportedBy!.id, - ), - ), + Text(l(context).reportedBy), + DisplayName( + widget.report.reportedBy!.name, + icon: widget.report.reportedBy!.avatar, + onTap: () => context.router.push( + UserRoute( + username: widget.report.reportedBy!.name, + userId: widget.report.reportedBy!.id, ), - ], - ), - Text('${l(context).reason}: ${item.reason}'), - Row( - children: [ - Text('${l(context).status}: '), - Text( - item.status.name, - style: TextStyle( - color: item.status == ReportStatus.pending - ? Colors.blue - : item.status == ReportStatus.approved - ? Colors.green - : Colors.red, - ), - ), - ], + ), ), ], ), - ), - Flex( - direction: Breakpoints.isCompact(context) - ? Axis.vertical - : Axis.horizontal, - spacing: 4, - children: [ - if (item.status != ReportStatus.approved) - LoadingOutlinedButton( - onPressed: () async { - final report = await context - .read() - .api - .communityModeration - .acceptReport(widget.data.id, item.id); - - _pagingController.updateItem(item, report); + Text('${l(context).reason}: ${widget.report.reason}'), + Row( + children: [ + Text('${l(context).status}: '), + Text( + switch (widget.report.status) { + ReportStatus.pending => l( + context, + ).reportStatus_pending, + ReportStatus.any => l(context).reportStatus_any, + ReportStatus.approved => l( + context, + ).reportStatus_deleted, + ReportStatus.rejected => l( + context, + ).reportStatus_restored, }, - label: Text(l(context).report_accept), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.green, + style: TextStyle( + color: widget.report.status == ReportStatus.pending + ? Colors.blue + : widget.report.status == ReportStatus.rejected + ? Colors.green + : Colors.red, ), ), - if (item.status != ReportStatus.rejected) - LoadingOutlinedButton( - onPressed: () async { - final report = await context - .read() - .api - .communityModeration - .rejectReport(widget.data.id, item.id); - - _pagingController.updateItem(item, report); - }, - label: Text(l(context).report_reject), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.red, - ), + ], + ), + ], + ), + ), + Flex( + direction: Breakpoints.isCompact(context) + ? Axis.vertical + : Axis.horizontal, + spacing: 4, + children: [ + LoadingOutlinedButton( + onPressed: () async { + await ac.api.communityModeration.createBan( + widget.communityId, + widget.report.reportedUser!.id, + ); + + widget.updateItem(null); + }, + label: Text( + l(context).banUserX(widget.report.reportedUser!.name), + ), + ), + if (widget.report.status != ReportStatus.approved) + Tooltip( + message: l(context).reportDeleteMessage, + child: LoadingOutlinedButton( + onPressed: () async { + final report = await ac.api.communityModeration + .acceptReport( + widget.communityId, + widget.report.id, + widget.report.subjectPost!.id, + ); + widget.updateItem(report); + }, + label: Text(l(context).delete), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.red, ), - LoadingOutlinedButton( + ), + ), + if (widget.report.status != ReportStatus.rejected) + Tooltip( + message: l(context).reportRestoreMessage, + child: LoadingOutlinedButton( onPressed: () async { - await context - .read() - .api - .communityModeration - .createBan(widget.data.id, item.reportedUser!.id); - - _pagingController.removeItem(item); + final report = await ac.api.communityModeration + .rejectReport( + widget.communityId, + widget.report.id, + widget.report.subjectPost!.id, + ); + widget.updateItem(report); }, - label: Text(l(context).banUserX(item.reportedUser!.name)), + label: Text(l(context).restore), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.green, + ), ), - ], - ), + ), ], ), - ), - ); - }, + ], + ), + ), ); } - - @override - void dispose() { - _pagingController.dispose(); - super.dispose(); - } } SelectionMenu reportStatusSelect(BuildContext context) => @@ -362,14 +482,16 @@ SelectionMenu reportStatusSelect(BuildContext context) => title: l(context).reportStatus_pending, icon: Symbols.schedule_rounded, ), - SelectionMenuItem( - value: ReportStatus.approved, - title: l(context).reportStatus_approved, - icon: Symbols.check_rounded, - ), - SelectionMenuItem( - value: ReportStatus.rejected, - title: l(context).reportStatus_rejected, - icon: Symbols.close_rounded, - ), + if (context.read().serverSoftware == ServerSoftware.mbin) + SelectionMenuItem( + value: ReportStatus.approved, + title: l(context).reportStatus_deleted, + icon: Symbols.check_rounded, + ), + if (context.read().serverSoftware == ServerSoftware.mbin) + SelectionMenuItem( + value: ReportStatus.rejected, + title: l(context).reportStatus_restored, + icon: Symbols.close_rounded, + ), ]); diff --git a/lib/src/screens/explore/community_owner_panel.dart b/lib/src/screens/explore/community_owner_panel.dart index 935feb42..2c1e5833 100644 --- a/lib/src/screens/explore/community_owner_panel.dart +++ b/lib/src/screens/explore/community_owner_panel.dart @@ -157,7 +157,7 @@ class _CommunityOwnerPanelGeneralState SwitchListTile( title: const Text('Is adult'), value: _isAdult, - onChanged: (bool value) { + onChanged: (value) { setState(() { _isAdult = value; }); @@ -166,7 +166,7 @@ class _CommunityOwnerPanelGeneralState SwitchListTile( title: const Text('Is posting restricted to mods'), value: _isPostingRestrictedToMods, - onChanged: (bool value) { + onChanged: (value) { setState(() { _isPostingRestrictedToMods = value; }); @@ -264,9 +264,9 @@ class _CommunityOwnerPanelModeratorsState .getByName(_addModController.text); if (!context.mounted) return; - final result = await showDialog( + final result = await showDialog>( context: context, - builder: (BuildContext context) => AlertDialog( + builder: (context) => AlertDialog( title: const Text('Add Moderator'), content: Column( mainAxisSize: MainAxisSize.min, @@ -299,7 +299,11 @@ class _CommunityOwnerPanelModeratorsState ), ); - if (result != null) widget.onUpdate(result); + if (result != null) { + widget.onUpdate( + widget.data.copyWith(moderators: result), + ); + } }, label: const Text('Add'), icon: const Icon(Symbols.add_rounded), @@ -317,7 +321,7 @@ class _CommunityOwnerPanelModeratorsState onPressed: () async { final result = await showDialog( context: context, - builder: (BuildContext context) => AlertDialog( + builder: (context) => AlertDialog( title: const Text('Remove moderator'), content: Column( mainAxisSize: MainAxisSize.min, @@ -411,7 +415,7 @@ class _CommunityOwnerPanelDeletionState onPressed: () async { final result = await showDialog( context: context, - builder: (BuildContext context) => + builder: (context) => CommunityOwnerPanelDeletionDialog(data: widget.data), ); diff --git a/lib/src/screens/explore/community_screen.dart b/lib/src/screens/explore/community_screen.dart index 9ee0976a..79be560a 100644 --- a/lib/src/screens/explore/community_screen.dart +++ b/lib/src/screens/explore/community_screen.dart @@ -48,21 +48,21 @@ class _CommunityScreenState extends State { _data = widget.initData; - if (_data == null) { - (widget.communityId == null - ? context.read().api.community.getByName( - widget.communityName, - ) - : context.read().api.community.get( - widget.communityId!, - )) - .then((value) { - if (!mounted) return; - setState(() { - _data = value; - }); + // if (_data == null) { + (widget.communityId == null + ? context.read().api.community.getByName( + widget.communityName, + ) + : context.read().api.community.get( + widget.communityId!, + )) + .then((value) { + if (!mounted) return; + setState(() { + _data = value; }); - } + }); + // } } @override diff --git a/lib/src/screens/feed/post_comment.dart b/lib/src/screens/feed/post_comment.dart index 6f2494f7..2a55d704 100644 --- a/lib/src/screens/feed/post_comment.dart +++ b/lib/src/screens/feed/post_comment.dart @@ -8,6 +8,7 @@ import 'package:interstellar/src/controller/controller.dart'; import 'package:interstellar/src/controller/router.gr.dart'; import 'package:interstellar/src/controller/server.dart'; import 'package:interstellar/src/models/comment.dart'; +import 'package:interstellar/src/models/post.dart'; import 'package:interstellar/src/utils/ap_urls.dart'; import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/widgets/ban_dialog.dart'; @@ -185,7 +186,7 @@ class _PostCommentState extends State { reason, ); }), - onEdit: widget.comment.visibility != 'soft_deleted' + onEdit: widget.comment.visibility != PostVisibility.soft_deleted ? whenLoggedIn(context, (body) async { final newValue = await ac.api.comments.edit( widget.comment.postType, @@ -201,7 +202,7 @@ class _PostCommentState extends State { ); }, matchesUsername: widget.comment.user.name) : null, - onDelete: widget.comment.visibility != 'soft_deleted' + onDelete: widget.comment.visibility != PostVisibility.soft_deleted ? whenLoggedIn(context, () async { await ac.api.comments.delete( widget.comment.postType, @@ -216,7 +217,7 @@ class _PostCommentState extends State { upvotes: null, downvotes: null, boosts: null, - visibility: 'soft_deleted', + visibility: PostVisibility.soft_deleted, ), ); }, matchesUsername: widget.comment.user.name) diff --git a/lib/src/screens/feed/post_page.dart b/lib/src/screens/feed/post_page.dart index 99fc0903..559c3b95 100644 --- a/lib/src/screens/feed/post_page.dart +++ b/lib/src/screens/feed/post_page.dart @@ -513,7 +513,7 @@ class _PostPageState extends State { _mainCommentSectionKey.currentState?._pagingController .prependPage('', [newComment]); }), - onEdit: post.visibility != 'soft_deleted' + onEdit: post.visibility != PostVisibility.soft_deleted ? whenLoggedIn(context, (body) async { final newPost = await switch (post.type) { PostType.thread => ac.api.threads.edit( @@ -534,7 +534,7 @@ class _PostPageState extends State { _onUpdate(newPost); }, matchesUsername: post.user.name) : null, - onDelete: post.visibility != 'soft_deleted' + onDelete: post.visibility != PostVisibility.soft_deleted ? whenLoggedIn(context, () async { await switch (post.type) { PostType.thread => ac.api.threads.delete(post.id), @@ -551,7 +551,7 @@ class _PostPageState extends State { upvotes: null, downvotes: null, boosts: null, - visibility: 'soft_deleted', + visibility: PostVisibility.soft_deleted, ), ); }, matchesUsername: post.user.name) diff --git a/lib/src/widgets/menus/content_menu.dart b/lib/src/widgets/menus/content_menu.dart index 86588690..77008c2d 100644 --- a/lib/src/widgets/menus/content_menu.dart +++ b/lib/src/widgets/menus/content_menu.dart @@ -346,7 +346,6 @@ Future showContentMenu( widget.onModerateLock != null) ContextMenuItem( title: l(context).moderate, - // onTap: () async => showModerateMenu(context, widget), subItems: [ if (widget.onModeratePin != null) ContextMenuItem(