diff --git a/lib/core/singletons/lemmy_client.dart b/lib/core/singletons/lemmy_client.dart index 4a5ccdfbe..7ea65c1c7 100644 --- a/lib/core/singletons/lemmy_client.dart +++ b/lib/core/singletons/lemmy_client.dart @@ -95,6 +95,7 @@ enum LemmyFeature { multiRead(0, 19, 0, preRelease: ["rc", "1"]), hidePosts(0, 19, 4), customThumbnail(0, 19, 4), + commentModLog(0, 19, 4), imageDimension(0, 19, 6), ; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d53a38ada..5cb2bf6c8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2813,6 +2813,10 @@ "@viewCommentSource": { "description": "Menu item for viewing a comment's source" }, + "viewModlog": "View Modlog", + "@viewModlog": { + "description": "Action which allows the user to navigate to the modlog for a particular entity" + }, "viewOriginal": "View original", "@viewOriginal": { "description": "Action for viewing original text (as opposed to raw markdown)" diff --git a/lib/modlog/bloc/modlog_bloc.dart b/lib/modlog/bloc/modlog_bloc.dart index 47a1588c7..87f59cb2d 100644 --- a/lib/modlog/bloc/modlog_bloc.dart +++ b/lib/modlog/bloc/modlog_bloc.dart @@ -75,6 +75,7 @@ class ModlogBloc extends Bloc { communityId: state.communityId, userId: state.userId, moderatorId: state.moderatorId, + commentId: state.commentId, reset: true, )); } @@ -91,6 +92,7 @@ class ModlogBloc extends Bloc { communityId: event.communityId, userId: event.userId, moderatorId: event.moderatorId, + commentId: event.commentId, lemmyClient: lemmyClient, ); @@ -130,6 +132,7 @@ class ModlogBloc extends Bloc { communityId: state.communityId, userId: state.userId, moderatorId: state.moderatorId, + commentId: event.commentId, lemmyClient: lemmyClient, ); diff --git a/lib/modlog/bloc/modlog_event.dart b/lib/modlog/bloc/modlog_event.dart index 74994275e..20a821c2c 100644 --- a/lib/modlog/bloc/modlog_event.dart +++ b/lib/modlog/bloc/modlog_event.dart @@ -24,6 +24,9 @@ final class ModlogFeedFetchedEvent extends ModlogEvent { /// The id of the moderator to display posts for. final int? moderatorId; + /// The id of a specific comment to show in the modlog (optional) + final int? commentId; + /// Boolean which indicates whether or not to reset the feed final bool reset; @@ -32,6 +35,7 @@ final class ModlogFeedFetchedEvent extends ModlogEvent { this.communityId, this.userId, this.moderatorId, + this.commentId, this.reset = false, }); } diff --git a/lib/modlog/bloc/modlog_state.dart b/lib/modlog/bloc/modlog_state.dart index 4575d4e68..9f1db0c44 100644 --- a/lib/modlog/bloc/modlog_state.dart +++ b/lib/modlog/bloc/modlog_state.dart @@ -9,6 +9,7 @@ final class ModlogState extends Equatable { this.communityId, this.userId, this.moderatorId, + this.commentId, this.modlogEventItems = const [], this.hasReachedEnd = false, this.currentPage = 1, @@ -30,6 +31,9 @@ final class ModlogState extends Equatable { /// The id of the moderator to display modlog events for. final int? moderatorId; + /// The id of a specific comment to show in the modlog (optional) + final int? commentId; + /// The list of modlog events final List modlogEventItems; @@ -48,6 +52,7 @@ final class ModlogState extends Equatable { int? communityId, int? userId, int? moderatorId, + int? commentId, List? modlogEventItems, bool? hasReachedEnd, int? currentPage, @@ -59,6 +64,7 @@ final class ModlogState extends Equatable { communityId: communityId ?? this.communityId, userId: userId ?? this.userId, moderatorId: moderatorId ?? this.moderatorId, + commentId: commentId ?? this.commentId, modlogEventItems: modlogEventItems ?? this.modlogEventItems, hasReachedEnd: hasReachedEnd ?? this.hasReachedEnd, currentPage: currentPage ?? this.currentPage, @@ -68,9 +74,9 @@ final class ModlogState extends Equatable { @override String toString() { - return '''ModlogState { status: $status, modlogActionType: $modlogActionType, communityId: $communityId, userId: $userId, moderatorId: $moderatorId, modlogEventItems: ${modlogEventItems.length}, hasReachedEnd: $hasReachedEnd }'''; + return '''ModlogState { status: $status, modlogActionType: $modlogActionType, communityId: $communityId, userId: $userId, moderatorId: $moderatorId, commentId: $commentId, modlogEventItems: ${modlogEventItems.length}, hasReachedEnd: $hasReachedEnd }'''; } @override - List get props => [status, modlogActionType, communityId, userId, moderatorId, modlogEventItems, hasReachedEnd, currentPage, message]; + List get props => [status, modlogActionType, communityId, userId, moderatorId, commentId, modlogEventItems, hasReachedEnd, currentPage, message]; } diff --git a/lib/modlog/utils/modlog.dart b/lib/modlog/utils/modlog.dart index 6c64d373e..899fac3c8 100644 --- a/lib/modlog/utils/modlog.dart +++ b/lib/modlog/utils/modlog.dart @@ -15,6 +15,7 @@ Future> fetchModlogEvents({ int? communityId, int? userId, int? moderatorId, + int? commentId, required LemmyClient lemmyClient, }) async { Account? account = await fetchActiveProfileAccount(); @@ -34,6 +35,7 @@ Future> fetchModlogEvents({ communityId: communityId, otherPersonId: userId, modPersonId: moderatorId, + commentId: commentId, )); List items = []; diff --git a/lib/modlog/utils/navigate_modlog.dart b/lib/modlog/utils/navigate_modlog.dart index 991cbc66b..9ef903151 100644 --- a/lib/modlog/utils/navigate_modlog.dart +++ b/lib/modlog/utils/navigate_modlog.dart @@ -14,12 +14,14 @@ import 'package:thunder/utils/swipe.dart'; Future navigateToModlogPage( BuildContext context, { - required FeedBloc feedBloc, + FeedBloc? feedBloc, ModlogActionType? modlogActionType, int? communityId, int? userId, int? moderatorId, + int? commentId, LemmyClient? lemmyClient, + Widget? subtitle, }) async { final ThunderBloc thunderBloc = context.read(); final bool reduceAnimations = thunderBloc.state.reduceAnimations; @@ -42,7 +44,7 @@ Future navigateToModlogPage( canOnlySwipeFromEdge: canOnlySwipeFromEdge, builder: (context) => MultiBlocProvider( providers: [ - BlocProvider.value(value: feedBloc), + if (feedBloc != null) BlocProvider.value(value: feedBloc), BlocProvider.value(value: thunderBloc), ], child: ModlogFeedPage( @@ -50,7 +52,9 @@ Future navigateToModlogPage( communityId: communityId, userId: userId, moderatorId: moderatorId, + commentId: commentId, lemmyClient: lemmyClient, + subtitle: subtitle, ), ), ), diff --git a/lib/modlog/view/modlog_page.dart b/lib/modlog/view/modlog_page.dart index f15a5a382..2188fd2ed 100644 --- a/lib/modlog/view/modlog_page.dart +++ b/lib/modlog/view/modlog_page.dart @@ -10,9 +10,11 @@ import 'package:thunder/core/enums/font_scale.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/feed/feed.dart'; import 'package:thunder/modlog/modlog.dart'; +import 'package:thunder/shared/full_name_widgets.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/shared/text/scalable_text.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; +import 'package:thunder/utils/instance.dart'; /// Creates a [ModlogPage] which holds a list of modlog events. class ModlogFeedPage extends StatefulWidget { @@ -23,6 +25,8 @@ class ModlogFeedPage extends StatefulWidget { this.userId, this.moderatorId, this.lemmyClient, + this.subtitle, + this.commentId, }); /// The filtering to be applied to the feed. @@ -37,9 +41,16 @@ class ModlogFeedPage extends StatefulWidget { /// The id of the moderator to display modlog events for. final int? moderatorId; + /// The id of a specific comment to show in the modlog (optional) + final int? commentId; + /// An optional lemmy client to use a different instance and override the singleton final LemmyClient? lemmyClient; + /// An optional widget to display as the subtitle on the app bar. + /// If not specified, this will be the instance or community name. + final Widget? subtitle; + @override State createState() => _ModlogFeedPageState(); } @@ -54,9 +65,10 @@ class _ModlogFeedPageState extends State { communityId: widget.communityId, userId: widget.userId, moderatorId: widget.moderatorId, + commentId: widget.commentId, reset: true, )), - child: ModlogFeedView(lemmyClient: widget.lemmyClient ?? LemmyClient.instance), + child: ModlogFeedView(lemmyClient: widget.lemmyClient ?? LemmyClient.instance, subtitle: widget.subtitle), ); } } @@ -65,7 +77,10 @@ class ModlogFeedView extends StatefulWidget { /// The current Lemmy client final LemmyClient lemmyClient; - const ModlogFeedView({super.key, required this.lemmyClient}); + /// Subtitle to display on app bar + final Widget? subtitle; + + const ModlogFeedView({super.key, required this.lemmyClient, required this.subtitle}); @override State createState() => _ModlogFeedViewState(); @@ -130,6 +145,25 @@ class _ModlogFeedViewState extends State { final thunderState = context.watch().state; final l10n = AppLocalizations.of(context)!; + Widget? subtitle = widget.subtitle; + + if (subtitle == null) { + try { + FeedState feedState = context.read().state; + + subtitle = feedState.fullCommunityView != null + ? CommunityFullNameWidget( + context, + feedState.fullCommunityView!.communityView.community.name, + feedState.fullCommunityView!.communityView.community.title, + fetchInstanceNameFromUrl(feedState.fullCommunityView!.communityView.community.actorId), + ) + : Text(widget.lemmyClient.lemmyApiV3.host); + } catch (e) { + // Ignore if we can't get the FeedBloc from this context + } + } + return Scaffold( body: SafeArea( top: thunderState.hideTopBarOnScroll, // Don't apply to top of screen to allow for the status bar colour to extend @@ -163,9 +197,14 @@ class _ModlogFeedViewState extends State { return RefreshIndicator( onRefresh: () async { HapticFeedback.mediumImpact(); - context - .read() - .add(ModlogFeedFetchedEvent(modlogActionType: state.modlogActionType, communityId: state.communityId, userId: state.userId, moderatorId: state.moderatorId, reset: true)); + context.read().add(ModlogFeedFetchedEvent( + modlogActionType: state.modlogActionType, + communityId: state.communityId, + userId: state.userId, + moderatorId: state.moderatorId, + commentId: state.commentId, + reset: true, + )); }, edgeOffset: 95.0, // This offset is placed to allow the correct positioning of the refresh indicator child: Stack( @@ -175,7 +214,7 @@ class _ModlogFeedViewState extends State { slivers: [ ModlogFeedPageAppBar( showAppBarTitle: state.status != ModlogStatus.initial ? true : showAppBarTitle, - lemmyClient: widget.lemmyClient, + subtitle: subtitle, ), // Display loading indicator until the feed is fetched if (state.status == ModlogStatus.initial) diff --git a/lib/modlog/widgets/modlog_feed_page_app_bar.dart b/lib/modlog/widgets/modlog_feed_page_app_bar.dart index 7c1b9df2e..732ef3735 100644 --- a/lib/modlog/widgets/modlog_feed_page_app_bar.dart +++ b/lib/modlog/widgets/modlog_feed_page_app_bar.dart @@ -6,23 +6,19 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:thunder/core/singletons/lemmy_client.dart'; -import 'package:thunder/feed/bloc/feed_bloc.dart'; import 'package:thunder/modlog/bloc/modlog_bloc.dart'; import 'package:thunder/modlog/widgets/modlog_filter_picker.dart'; -import 'package:thunder/shared/full_name_widgets.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; -import 'package:thunder/utils/instance.dart'; /// The app bar for the modlog feed page class ModlogFeedPageAppBar extends StatelessWidget { - const ModlogFeedPageAppBar({super.key, required this.showAppBarTitle, required this.lemmyClient}); + const ModlogFeedPageAppBar({super.key, required this.showAppBarTitle, required this.subtitle}); /// Boolean which indicates whether the title on the app bar should be shown final bool showAppBarTitle; - /// The current Lemmy client - final LemmyClient lemmyClient; + /// The subtitle to display below "Modlog" on the app bar + final Widget? subtitle; @override Widget build(BuildContext context) { @@ -35,7 +31,7 @@ class ModlogFeedPageAppBar extends StatelessWidget { centerTitle: false, toolbarHeight: 70.0, surfaceTintColor: state.hideTopBarOnScroll ? Colors.transparent : null, - title: ModlogFeedAppBarTitle(visible: showAppBarTitle, lemmyClient: lemmyClient), + title: ModlogFeedAppBarTitle(visible: showAppBarTitle, subtitle: subtitle), leading: IconButton( icon: (!kIsWeb && Platform.isIOS ? Icon( @@ -75,21 +71,23 @@ class ModlogFeedPageAppBar extends StatelessWidget { } class ModlogFeedAppBarTitle extends StatelessWidget { - const ModlogFeedAppBarTitle({super.key, this.visible = true, required this.lemmyClient}); + const ModlogFeedAppBarTitle({ + super.key, + this.visible = true, + required this.subtitle, + }); /// Boolean which indicates whether the title on the app bar should be shown final bool visible; - /// The current Lemmy client - final LemmyClient lemmyClient; + /// The subtitle to display below "Modlog" on the app bar + final Widget? subtitle; @override Widget build(BuildContext context) { final theme = Theme.of(context); final l10n = AppLocalizations.of(context)!; - final feedState = context.read().state; - return AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: visible ? 1.0 : 0.0, @@ -100,14 +98,7 @@ class ModlogFeedAppBarTitle extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, ), - subtitle: feedState.fullCommunityView != null - ? CommunityFullNameWidget( - context, - feedState.fullCommunityView!.communityView.community.name, - feedState.fullCommunityView!.communityView.community.title, - fetchInstanceNameFromUrl(feedState.fullCommunityView!.communityView.community.actorId), - ) - : Text(lemmyClient.lemmyApiV3.host), + subtitle: subtitle, contentPadding: const EdgeInsets.symmetric(horizontal: 0), ), ); diff --git a/lib/post/utils/comment_action_helpers.dart b/lib/post/utils/comment_action_helpers.dart index 5bf85bdd9..8b9a3723a 100644 --- a/lib/post/utils/comment_action_helpers.dart +++ b/lib/post/utils/comment_action_helpers.dart @@ -15,6 +15,7 @@ import 'package:thunder/feed/utils/utils.dart'; import 'package:thunder/feed/view/feed_page.dart'; import 'package:thunder/instance/bloc/instance_bloc.dart'; import 'package:thunder/instance/enums/instance_action.dart'; +import 'package:thunder/modlog/utils/navigate_modlog.dart'; import 'package:thunder/post/bloc/post_bloc.dart'; import 'package:thunder/post/utils/user_label_utils.dart'; import 'package:thunder/post/widgets/report_comment_dialog.dart'; @@ -46,6 +47,7 @@ enum CommentCardAction { selectText, copyText, viewSource, + viewModlog, report, userActions, visitProfile, @@ -155,6 +157,11 @@ final List commentCardDefaultActionItems = [ label: l10n.viewCommentSource, getOverrideLabel: (context, commentView, viewSource) => viewSource ? l10n.viewOriginal : l10n.viewCommentSource, ), + ExtendedCommentCardActions( + commentCardAction: CommentCardAction.viewModlog, + icon: Icons.shield_rounded, + label: AppLocalizations.of(GlobalContext.context)!.viewModlog, + ), ExtendedCommentCardActions( commentCardAction: CommentCardAction.report, icon: Icons.report_outlined, @@ -300,6 +307,7 @@ void showCommentActionBottomModalSheet( CommentCardAction.selectText, CommentCardAction.copyText, CommentCardAction.viewSource, + if (commentView.comment.removed && LemmyClient.instance.supportsFeature(LemmyFeature.commentModLog)) CommentCardAction.viewModlog, ].contains(extendedAction.commentCardAction)) .toList(); @@ -546,6 +554,16 @@ class _CommentActionPickerState extends State { case CommentCardAction.viewSource: action = widget.onViewSourceToggled; break; + case CommentCardAction.viewModlog: + action = () async { + await navigateToModlogPage( + context, + subtitle: Text(l10n.removedComment), + modlogActionType: ModlogActionType.modRemoveComment, + commentId: widget.commentView.comment.id, + ); + }; + break; case CommentCardAction.report: action = () => widget.onReportAction(widget.commentView.comment.id);