From c039de8cdb85d205671749eca6675c19f7aad6e8 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Sun, 25 Aug 2024 18:58:58 -0700 Subject: [PATCH 1/9] refactor: refactor post bottom sheet actions --- lib/comment/view/create_comment_page.dart | 2 +- lib/community/pages/create_post_page.dart | 2 +- .../utils/post_card_action_helpers.dart | 884 ---------------- lib/community/widgets/post_card.dart | 8 +- .../widgets/post_card_view_comfortable.dart | 8 +- lib/l10n/app_en.arb | 8 + .../community_post_action_bottom_sheet.dart | 184 ++++ .../general_post_action_bottom_sheet.dart | 90 ++ .../widgets/post_action_bottom_sheet.dart | 977 ++++++++++++++++++ lib/post/widgets/post_view.dart | 5 +- .../user_post_action_bottom_sheet.dart | 237 +++++ .../pages/accessibility_settings_page.dart | 4 +- lib/shared/bottom_sheet_action.dart | 46 + lib/user/bloc/user_bloc.dart | 42 + lib/user/bloc/user_event.dart | 5 +- lib/user/enums/user_action.dart | 5 +- lib/user/utils/user.dart | 43 + 17 files changed, 1649 insertions(+), 901 deletions(-) delete mode 100644 lib/community/utils/post_card_action_helpers.dart create mode 100644 lib/post/widgets/community_post_action_bottom_sheet.dart create mode 100644 lib/post/widgets/general_post_action_bottom_sheet.dart create mode 100644 lib/post/widgets/post_action_bottom_sheet.dart create mode 100644 lib/post/widgets/user_post_action_bottom_sheet.dart create mode 100644 lib/shared/bottom_sheet_action.dart diff --git a/lib/comment/view/create_comment_page.dart b/lib/comment/view/create_comment_page.dart index 182d99eb7..b55c35845 100644 --- a/lib/comment/view/create_comment_page.dart +++ b/lib/comment/view/create_comment_page.dart @@ -15,7 +15,7 @@ import 'package:thunder/account/models/draft.dart'; // Project imports import 'package:thunder/comment/cubit/create_comment_cubit.dart'; -import 'package:thunder/community/utils/post_card_action_helpers.dart'; +import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/models/post_view_media.dart'; import 'package:thunder/drafts/draft_type.dart'; diff --git a/lib/community/pages/create_post_page.dart b/lib/community/pages/create_post_page.dart index 71726fafa..370ff22af 100644 --- a/lib/community/pages/create_post_page.dart +++ b/lib/community/pages/create_post_page.dart @@ -18,7 +18,7 @@ import 'package:markdown_editor/markdown_editor.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/account/models/draft.dart'; import 'package:thunder/community/bloc/image_bloc.dart'; -import 'package:thunder/community/utils/post_card_action_helpers.dart'; +import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; import 'package:thunder/core/enums/view_mode.dart'; diff --git a/lib/community/utils/post_card_action_helpers.dart b/lib/community/utils/post_card_action_helpers.dart deleted file mode 100644 index 228c55e7b..000000000 --- a/lib/community/utils/post_card_action_helpers.dart +++ /dev/null @@ -1,884 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'package:back_button_interceptor/back_button_interceptor.dart'; -import 'package:flutter/material.dart'; - -import 'package:share_plus/share_plus.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:thunder/account/bloc/account_bloc.dart'; - -import 'package:thunder/community/bloc/community_bloc.dart'; -import 'package:thunder/community/enums/community_action.dart'; -import 'package:thunder/community/widgets/post_card_metadata.dart'; -import 'package:thunder/core/enums/full_name.dart'; -import 'package:thunder/core/enums/media_type.dart'; -import 'package:thunder/core/models/post_view_media.dart'; -import 'package:thunder/core/singletons/lemmy_client.dart'; -import 'package:thunder/feed/bloc/feed_bloc.dart'; -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/post/enums/post_action.dart'; -import 'package:thunder/post/widgets/reason_bottom_sheet.dart'; -import 'package:thunder/shared/advanced_share_sheet.dart'; -import 'package:thunder/shared/picker_item.dart'; -import 'package:thunder/shared/snackbar.dart'; -import 'package:thunder/thunder/bloc/thunder_bloc.dart'; -import 'package:thunder/user/bloc/user_bloc.dart'; -import 'package:thunder/user/enums/user_action.dart'; -import 'package:thunder/utils/instance.dart'; -import 'package:thunder/instance/utils/navigate_instance.dart'; -import 'package:lemmy_api_client/v3.dart'; - -import 'package:thunder/core/auth/bloc/auth_bloc.dart'; -import 'package:thunder/shared/multi_picker_item.dart'; -import 'package:thunder/utils/global_context.dart'; - -enum PostCardAction { - userActions, - visitProfile, - blockUser, - communityActions, - visitCommunity, - subscribeToCommunity, - unsubscribeFromCommunity, - blockCommunity, - instanceActions, - visitCommunityInstance, - blockCommunityInstance, - visitUserInstance, - blockUserInstance, - sharePost, - sharePostLocal, - shareImage, - shareMedia, - shareLink, - shareAdvanced, - upvote, - downvote, - save, - toggleRead, - hide, - share, - delete, - moderatorActions, - moderatorLockPost, - moderatorPinCommunity, - moderatorRemovePost, -} - -class ExtendedPostCardActions { - const ExtendedPostCardActions({ - required this.postCardAction, - required this.icon, - this.trailingIcon, - required this.label, - this.getColor, - this.getForegroundColor, - this.getOverrideIcon, - this.getOverrideLabel, - this.getSubtitleLabel, - this.shouldShow, - this.shouldEnable, - }); - - final PostCardAction postCardAction; - final IconData icon; - final IconData? trailingIcon; - final String label; - final Color Function(BuildContext context)? getColor; - final Color? Function(BuildContext context, PostView postView)? getForegroundColor; - final IconData? Function(PostView postView)? getOverrideIcon; - final String? Function(BuildContext context, PostView postView)? getOverrideLabel; - final String? Function(BuildContext context, PostViewMedia postViewMedia)? getSubtitleLabel; - final bool Function(BuildContext context, PostView commentView)? shouldShow; - final bool Function(bool isUserLoggedIn)? shouldEnable; -} - -final l10n = AppLocalizations.of(GlobalContext.context)!; - -final List postCardActionItems = [ - ExtendedPostCardActions( - postCardAction: PostCardAction.userActions, - icon: Icons.person_rounded, - label: l10n.user, - getSubtitleLabel: (context, postViewMedia) => generateUserFullName( - context, - postViewMedia.postView.creator.name, - postViewMedia.postView.creator.displayName, - fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId), - ), - trailingIcon: Icons.chevron_right_rounded, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.visitProfile, - icon: Icons.person_search_rounded, - label: l10n.visitUserProfile, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.blockUser, - icon: Icons.block, - label: l10n.blockUser, - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.communityActions, - icon: Icons.people_rounded, - label: l10n.community, - getSubtitleLabel: (context, postViewMedia) => generateCommunityFullName( - context, - postViewMedia.postView.community.name, - postViewMedia.postView.community.title, - fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), - ), - trailingIcon: Icons.chevron_right_rounded, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.visitCommunity, - icon: Icons.home_work_rounded, - label: l10n.visitCommunity, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.subscribeToCommunity, - icon: Icons.add_circle_outline_rounded, - label: l10n.subscribeToCommunity, - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.unsubscribeFromCommunity, - icon: Icons.remove_circle_outline_rounded, - label: l10n.unsubscribeFromCommunity, - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.blockCommunity, - icon: Icons.block_rounded, - label: l10n.blockCommunity, - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.instanceActions, - icon: Icons.language_rounded, - label: l10n.instance(1), - getSubtitleLabel: (context, postViewMedia) { - return areCommunityAndUserOnSameInstance(postViewMedia.postView) - ? fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId) - : '${fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId)} • ${fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId)}'; - }, - trailingIcon: Icons.chevron_right_rounded, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.visitCommunityInstance, - icon: Icons.language, - label: '', - getOverrideLabel: (context, postView) { - return areCommunityAndUserOnSameInstance(postView) ? l10n.visitInstance : l10n.visitCommunityInstance; - }, - getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.blockCommunityInstance, - icon: Icons.block_rounded, - label: '', - getOverrideLabel: (context, postView) { - return areCommunityAndUserOnSameInstance(postView) ? l10n.blockInstance : l10n.blockCommunityInstance; - }, - getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.visitUserInstance, - icon: Icons.language, - label: l10n.visitUserInstance, - getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId), - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.blockUserInstance, - icon: Icons.block_rounded, - label: l10n.blockUserInstance, - getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId), - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.sharePost, - icon: Icons.share_rounded, - label: l10n.sharePost, - getSubtitleLabel: (context, postViewMedia) => postViewMedia.postView.post.apId, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.sharePostLocal, - icon: Icons.share_rounded, - label: l10n.sharePostLocal, - getSubtitleLabel: (context, postViewMedia) => LemmyClient.instance.generatePostUrl(postViewMedia.postView.post.id), - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.shareImage, - icon: Icons.image_rounded, - label: l10n.shareImage, - getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.imageUrl, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.shareMedia, - icon: Icons.personal_video_rounded, - label: l10n.shareMediaLink, - getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.mediaUrl, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.shareLink, - icon: Icons.link_rounded, - label: l10n.shareLink, - getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.originalUrl, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.shareAdvanced, - icon: Icons.screen_share_rounded, - label: l10n.advanced, - getSubtitleLabel: (context, postViewMedia) => l10n.useAdvancedShareSheet, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.upvote, - label: l10n.upvote, - icon: Icons.arrow_upward_rounded, - getColor: (context) => context.read().state.upvoteColor.color, - getForegroundColor: (context, postView) => postView.myVote == 1 ? context.read().state.upvoteColor.color : null, - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.downvote, - label: l10n.downvote, - icon: Icons.arrow_downward_rounded, - getColor: (context) => context.read().state.downvoteColor.color, - getForegroundColor: (context, postView) => postView.myVote == -1 ? context.read().state.downvoteColor.color : null, - shouldShow: (context, commentView) => context.read().state.downvotesEnabled, - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.save, - label: l10n.save, - icon: Icons.star_border_rounded, - getColor: (context) => context.read().state.saveColor.color, - getForegroundColor: (context, postView) => postView.saved ? context.read().state.saveColor.color : null, - getOverrideIcon: (postView) => postView.saved ? Icons.star_rounded : null, - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.toggleRead, - label: l10n.toggelRead, - icon: Icons.mail_outline_outlined, - getColor: (context) => context.read().state.markReadColor.color, - getOverrideIcon: (postView) => postView.read ? Icons.mark_email_unread_rounded : Icons.mark_email_read_outlined, - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.hide, - label: l10n.hide, - getOverrideLabel: (context, postView) => postView.hidden == true ? l10n.unhide : l10n.hide, - icon: Icons.visibility_off_rounded, - getColor: (context) => context.read().state.hideColor.color, - getOverrideIcon: (postView) => postView.hidden == true ? Icons.visibility_rounded : Icons.visibility_off_rounded, - shouldEnable: (isUserLoggedIn) => isUserLoggedIn, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.share, - icon: Icons.share_rounded, - label: l10n.share, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.delete, - icon: Icons.delete_rounded, - label: l10n.delete, - getOverrideIcon: (postView) => postView.post.deleted ? Icons.restore_from_trash_rounded : Icons.delete_rounded, - getOverrideLabel: (context, postView) => postView.post.deleted ? l10n.restore : l10n.delete, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.moderatorActions, - icon: Icons.shield_rounded, - trailingIcon: Icons.chevron_right_rounded, - label: l10n.moderatorActions, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.moderatorLockPost, - icon: Icons.lock, - label: l10n.lockPost, - getOverrideIcon: (postView) => postView.post.locked ? Icons.lock_open_rounded : Icons.lock, - getOverrideLabel: (context, postView) => postView.post.locked ? l10n.unlockPost : l10n.lockPost, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.moderatorPinCommunity, - icon: Icons.push_pin_rounded, - label: l10n.pinToCommunity, - getOverrideIcon: (postView) => postView.post.featuredCommunity ? Icons.push_pin_rounded : Icons.push_pin_outlined, - getOverrideLabel: (context, postView) => postView.post.featuredCommunity ? l10n.unpinFromCommunity : l10n.pinToCommunity, - ), - ExtendedPostCardActions( - postCardAction: PostCardAction.moderatorRemovePost, - icon: Icons.delete_forever_rounded, - label: l10n.removePost, - getOverrideIcon: (postView) => postView.post.removed ? Icons.restore_from_trash_rounded : Icons.delete_forever_rounded, - getOverrideLabel: (context, postView) => postView.post.removed ? l10n.restorePost : l10n.removePost, - ) -]; - -enum PostActionBottomSheetPage { - general, - share, - moderator, - user, - community, - instance, -} - -void showPostActionBottomModalSheet( - BuildContext context, - PostViewMedia postViewMedia, { - PostActionBottomSheetPage page = PostActionBottomSheetPage.general, - void Function(int userId)? onBlockedUser, - void Function(int userId)? onBlockedCommunity, - void Function(int postId)? onPostHidden, -}) { - final bool isOwnPost = postViewMedia.postView.creator.id == context.read().state.account?.userId; - final bool isModerator = - context.read().state.moderates.any((CommunityModeratorView communityModeratorView) => communityModeratorView.community.id == postViewMedia.postView.community.id); - final int? currentUserId = context.read().state.account?.userId; - - // Generate the list of default actions for the general page - final List defaultPostCardActions = postCardActionItems - .where((extendedAction) => [ - PostCardAction.userActions, - PostCardAction.communityActions, - PostCardAction.instanceActions, - ].contains(extendedAction.postCardAction)) - .toList(); - - // Add the moderator actions submenu - if (isModerator) { - defaultPostCardActions.add(postCardActionItems.firstWhere((ExtendedPostCardActions extendedPostCardActions) => extendedPostCardActions.postCardAction == PostCardAction.moderatorActions)); - } - - // Generate the list of default multi actions - final List defaultMultiPostCardActions = postCardActionItems - .where((extendedAction) => [ - PostCardAction.upvote, - PostCardAction.downvote, - PostCardAction.save, - PostCardAction.toggleRead, - PostCardAction.hide, - PostCardAction.share, - if (isOwnPost) PostCardAction.delete, - ].contains(extendedAction.postCardAction)) - .toList(); - - // Remove hide if unsupported - if (defaultMultiPostCardActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.hide) && !LemmyClient.instance.supportsFeature(LemmyFeature.hidePosts)) { - defaultMultiPostCardActions.removeWhere((ExtendedPostCardActions postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.hide); - } - - // Generate the list of moderator actions - final List moderatorPostCardActions = postCardActionItems - .where((extendedAction) => [ - PostCardAction.moderatorLockPost, - PostCardAction.moderatorPinCommunity, - PostCardAction.moderatorRemovePost, - ].contains(extendedAction.postCardAction)) - .toList(); - - // Generate the list of share actions - final List sharePostCardActions = postCardActionItems - .where((extendedAction) => [ - PostCardAction.sharePost, - PostCardAction.sharePostLocal, - PostCardAction.shareImage, - PostCardAction.shareMedia, - PostCardAction.shareLink, - PostCardAction.shareAdvanced, - ].contains(extendedAction.postCardAction)) - .toList(); - - // Remove the share link option if there is no link - // Or if the media link is the same as the external link - if (postViewMedia.media.isEmpty || - postViewMedia.media.first.mediaType == MediaType.text || - postViewMedia.media.first.originalUrl == postViewMedia.media.first.imageUrl || - postViewMedia.media.first.originalUrl == postViewMedia.media.first.mediaUrl) { - sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareLink); - } - - // Remove the share image option if there is no image - if (postViewMedia.media.isEmpty || postViewMedia.media.first.imageUrl?.isNotEmpty != true) { - sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareImage); - } - - // Remove the share media option if there is no media - if (postViewMedia.media.isEmpty || postViewMedia.media.first.mediaUrl?.isNotEmpty != true) { - sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareMedia); - } - - // Remove the share local option if it is the same as the original - if (postViewMedia.postView.post.apId == LemmyClient.instance.generatePostUrl(postViewMedia.postView.post.id)) { - sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.sharePostLocal); - } - - // Generate the list of user actions - final List userActions = postCardActionItems - .where((extendedAction) => [ - PostCardAction.visitProfile, - if (postViewMedia.postView.creator.id != currentUserId) PostCardAction.blockUser, - ].contains(extendedAction.postCardAction)) - .toList(); - - // Generate the list of community actions - final List communityActions = postCardActionItems - .where((extendedAction) => [ - PostCardAction.visitCommunity, - postViewMedia.postView.subscribed == SubscribedType.notSubscribed ? PostCardAction.subscribeToCommunity : PostCardAction.unsubscribeFromCommunity, - PostCardAction.blockCommunity, - ].contains(extendedAction.postCardAction)) - .toList(); - - // Hide the option to block a community if the user is subscribed to it - if (communityActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockCommunity) && postViewMedia.postView.subscribed != SubscribedType.notSubscribed) { - communityActions.removeWhere((ExtendedPostCardActions postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockCommunity); - } - - // Generate the list of instance actions - final List instanceActions = postCardActionItems - .where((extendedAction) => [ - PostCardAction.visitCommunityInstance, - PostCardAction.blockCommunityInstance, - PostCardAction.visitUserInstance, - PostCardAction.blockUserInstance, - ].contains(extendedAction.postCardAction)) - .toList(); - - // Remove block if unsupported - if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockCommunityInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { - instanceActions.removeWhere((ExtendedPostCardActions postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockCommunityInstance); - } - if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockUserInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { - instanceActions.removeWhere((ExtendedPostCardActions postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockUserInstance); - } - - // Hide user block if user's instance is the same as the community' sinstance - bool areSameInstance = areCommunityAndUserOnSameInstance(postViewMedia.postView); - if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.visitUserInstance) && areSameInstance) { - instanceActions.removeWhere((ExtendedPostCardActions postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.visitUserInstance); - } - if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockUserInstance) && areSameInstance) { - instanceActions.removeWhere((ExtendedPostCardActions postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockUserInstance); - } - - showModalBottomSheet( - showDragHandle: true, - isScrollControlled: true, - context: context, - builder: (builderContext) => PostCardActionPicker( - postViewMedia: postViewMedia, - page: page, - postCardActions: { - PostActionBottomSheetPage.general: defaultPostCardActions, - PostActionBottomSheetPage.moderator: moderatorPostCardActions, - PostActionBottomSheetPage.share: sharePostCardActions, - PostActionBottomSheetPage.user: userActions, - PostActionBottomSheetPage.community: communityActions, - PostActionBottomSheetPage.instance: instanceActions, - }, - multiPostCardActions: {PostActionBottomSheetPage.general: defaultMultiPostCardActions}, - titles: { - PostActionBottomSheetPage.general: l10n.actions, - PostActionBottomSheetPage.moderator: l10n.moderatorActions, - PostActionBottomSheetPage.share: l10n.share, - PostActionBottomSheetPage.user: l10n.userActions, - PostActionBottomSheetPage.community: l10n.communityActions, - PostActionBottomSheetPage.instance: l10n.instanceActions, - }, - outerContext: context, - onBlockedUser: onBlockedUser, - onBlockedCommunity: onBlockedCommunity, - onPostHidden: onPostHidden, - ), - ); -} - -class PostCardActionPicker extends StatefulWidget { - /// The post - final PostViewMedia postViewMedia; - - /// This is the list of quick actions that are shown horizontally across the top of the sheet - final Map> multiPostCardActions; - - /// This is the set of full actions to display vertically in a list - final Map> postCardActions; - - /// This is the set of titles to show for each page - final Map titles; - - /// The current page - final PostActionBottomSheetPage page; - - /// The context from whoever invoked this sheet (useful for blocs that would otherwise be missing) - final BuildContext outerContext; - - /// Callback used to notify that we blocked a user - final void Function(int userId)? onBlockedUser; - - /// Callback used to notify that we blocked a community - final Function(int userId)? onBlockedCommunity; - - /// Callback used to notify that we hid a post - final Function(int postId)? onPostHidden; - - const PostCardActionPicker({ - super.key, - required this.postViewMedia, - required this.page, - required this.postCardActions, - required this.multiPostCardActions, - required this.titles, - required this.outerContext, - required this.onBlockedUser, - required this.onBlockedCommunity, - required this.onPostHidden, - }); - - @override - State createState() => _PostCardActionPickerState(); -} - -class _PostCardActionPickerState extends State { - PostActionBottomSheetPage? page; - - @override - void initState() { - super.initState(); - - BackButtonInterceptor.add(_handleBack); - } - - @override - void dispose() { - BackButtonInterceptor.remove(_handleBack); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - final bool isUserLoggedIn = context.read().state.isLoggedIn; - - return SingleChildScrollView( - child: AnimatedSize( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Semantics( - label: '${widget.titles[page ?? widget.page] ?? l10n.actions}, ${(page ?? widget.page) == PostActionBottomSheetPage.general ? '' : l10n.backButton}', - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: Material( - borderRadius: BorderRadius.circular(50), - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(50), - onTap: (page ?? widget.page) == PostActionBottomSheetPage.general ? null : () => setState(() => page = PostActionBottomSheetPage.general), - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 10, 16.0, 10.0), - child: Align( - alignment: Alignment.centerLeft, - child: Row( - children: [ - if ((page ?? widget.page) != PostActionBottomSheetPage.general) ...[ - const Icon(Icons.chevron_left, size: 30), - const SizedBox(width: 12), - ], - Semantics( - excludeSemantics: true, - child: Text( - widget.titles[page ?? widget.page] ?? l10n.actions, - style: theme.textTheme.titleLarge, - ), - ), - ], - ), - ), - ), - ), - ), - ), - ), - // Post metadata chips - if ((page ?? PostActionBottomSheetPage.general) == PostActionBottomSheetPage.general) - Row( - children: [ - const SizedBox(width: 20), - LanguagePostCardMetaData(languageId: widget.postViewMedia.postView.post.languageId), - ], - ), - if (widget.multiPostCardActions[page ?? widget.page]?.isNotEmpty == true) - MultiPickerItem( - pickerItems: [ - ...widget.multiPostCardActions[page ?? widget.page]!.where((a) => a.shouldShow?.call(context, widget.postViewMedia.postView) ?? true).map( - (a) { - return PickerItemData( - label: a.getOverrideLabel?.call(context, widget.postViewMedia.postView) ?? a.label, - icon: a.getOverrideIcon?.call(widget.postViewMedia.postView) ?? a.icon, - backgroundColor: a.getColor?.call(context), - foregroundColor: a.getForegroundColor?.call(context, widget.postViewMedia.postView), - onSelected: (a.shouldEnable?.call(isUserLoggedIn) ?? true) ? () => onSelected(a.postCardAction) : null, - ); - }, - ), - ], - ), - if (widget.postCardActions[page ?? widget.page]?.isNotEmpty == true) - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: widget.postCardActions[page ?? widget.page]!.length, - itemBuilder: (BuildContext itemBuilderContext, int index) { - return PickerItem( - label: widget.postCardActions[page ?? widget.page]![index].getOverrideLabel?.call(context, widget.postViewMedia.postView) ?? - widget.postCardActions[page ?? widget.page]![index].label, - subtitle: widget.postCardActions[page ?? widget.page]![index].getSubtitleLabel?.call(context, widget.postViewMedia), - icon: widget.postCardActions[page ?? widget.page]![index].getOverrideIcon?.call(widget.postViewMedia.postView) ?? widget.postCardActions[page ?? widget.page]![index].icon, - trailingIcon: widget.postCardActions[page ?? widget.page]![index].trailingIcon, - onSelected: (widget.postCardActions[page ?? widget.page]![index].shouldEnable?.call(isUserLoggedIn) ?? true) - ? () => onSelected(widget.postCardActions[page ?? widget.page]![index].postCardAction) - : null, - ); - }, - ), - const SizedBox(height: 16.0), - ], - ), - ), - ), - ); - } - - void onSelected(PostCardAction postCardAction) async { - bool pop = true; - void Function() action; - - switch (postCardAction) { - case PostCardAction.visitCommunity: - action = () => onTapCommunityName(widget.outerContext, widget.postViewMedia.postView.community.id); - break; - case PostCardAction.userActions: - action = () => setState(() => page = PostActionBottomSheetPage.user); - pop = false; - break; - case PostCardAction.visitProfile: - action = () => navigateToFeedPage(widget.outerContext, feedType: FeedType.user, userId: widget.postViewMedia.postView.post.creatorId); - break; - case PostCardAction.visitCommunityInstance: - action = () => navigateToInstancePage(widget.outerContext, - instanceHost: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId)!, instanceId: widget.postViewMedia.postView.community.instanceId); - break; - case PostCardAction.visitUserInstance: - action = () => navigateToInstancePage(widget.outerContext, - instanceHost: fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId)!, instanceId: widget.postViewMedia.postView.creator.instanceId); - break; - case PostCardAction.sharePost: - action = () => Share.share(widget.postViewMedia.postView.post.apId); - break; - case PostCardAction.sharePostLocal: - action = () => Share.share(LemmyClient.instance.generatePostUrl(widget.postViewMedia.postView.post.id)); - break; - case PostCardAction.shareImage: - action = () async { - if (widget.postViewMedia.media.first.imageUrl != null) { - try { - // Try to get the cached image first - var media = await DefaultCacheManager().getFileFromCache(widget.postViewMedia.media.first.imageUrl!); - File? mediaFile = media?.file; - - if (media == null) { - // Tell user we're downloading the image - showSnackbar(AppLocalizations.of(widget.outerContext)!.downloadingMedia); - - // Download - mediaFile = await DefaultCacheManager().getSingleFile(widget.postViewMedia.media.first.imageUrl!); - } - - // Share - await Share.shareXFiles([XFile(mediaFile!.path)]); - } catch (e) { - // Tell the user that the download failed - showSnackbar(AppLocalizations.of(widget.outerContext)!.errorDownloadingMedia(e)); - } - } - }; - break; - case PostCardAction.shareMedia: - action = () => Share.share(widget.postViewMedia.media.first.mediaUrl!); - break; - case PostCardAction.shareLink: - action = () { - if (widget.postViewMedia.media.first.originalUrl != null) Share.share(widget.postViewMedia.media.first.originalUrl!); - }; - break; - case PostCardAction.shareAdvanced: - action = () => showAdvancedShareSheet(widget.outerContext, widget.postViewMedia); - break; - case PostCardAction.instanceActions: - action = () => setState(() => page = PostActionBottomSheetPage.instance); - pop = false; - break; - case PostCardAction.blockCommunityInstance: - action = () => widget.outerContext.read().add(InstanceActionEvent( - instanceAction: InstanceAction.block, - instanceId: widget.postViewMedia.postView.community.instanceId, - domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId), - value: true, - )); - break; - case PostCardAction.blockUserInstance: - action = () => widget.outerContext.read().add(InstanceActionEvent( - instanceAction: InstanceAction.block, - instanceId: widget.postViewMedia.postView.creator.instanceId, - domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId), - value: true, - )); - break; - case PostCardAction.communityActions: - action = () => setState(() => page = PostActionBottomSheetPage.community); - pop = false; - break; - case PostCardAction.blockCommunity: - action = () { - widget.outerContext.read().add(CommunityActionEvent(communityAction: CommunityAction.block, communityId: widget.postViewMedia.postView.community.id, value: true)); - widget.onBlockedCommunity?.call(widget.postViewMedia.postView.community.id); - }; - break; - case PostCardAction.upvote: - action = () => widget.outerContext - .read() - .add(FeedItemActionedEvent(postAction: PostAction.vote, postId: widget.postViewMedia.postView.post.id, value: widget.postViewMedia.postView.myVote == 1 ? 0 : 1)); - break; - case PostCardAction.downvote: - action = () => widget.outerContext - .read() - .add(FeedItemActionedEvent(postAction: PostAction.vote, postId: widget.postViewMedia.postView.post.id, value: widget.postViewMedia.postView.myVote == -1 ? 0 : -1)); - break; - case PostCardAction.save: - action = () => - widget.outerContext.read().add(FeedItemActionedEvent(postAction: PostAction.save, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.saved)); - break; - case PostCardAction.toggleRead: - action = () => - widget.outerContext.read().add(FeedItemActionedEvent(postAction: PostAction.read, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.read)); - break; - case PostCardAction.hide: - action = () => widget.outerContext - .read() - .add(FeedItemActionedEvent(postAction: PostAction.hide, postId: widget.postViewMedia.postView.post.id, value: !(widget.postViewMedia.postView.hidden ?? false))); - widget.onPostHidden?.call(widget.postViewMedia.postView.post.id); - break; - case PostCardAction.share: - pop = false; - action = () => setState(() => page = PostActionBottomSheetPage.share); - break; - case PostCardAction.blockUser: - action = () { - widget.outerContext.read().add(UserActionEvent(userAction: UserAction.block, userId: widget.postViewMedia.postView.creator.id, value: true)); - widget.onBlockedCommunity?.call(widget.postViewMedia.postView.creator.id); - }; - break; - case PostCardAction.subscribeToCommunity: - action = () => widget.outerContext.read().add(CommunityActionEvent( - communityAction: CommunityAction.follow, - communityId: widget.postViewMedia.postView.community.id, - value: true, - )); - break; - case PostCardAction.unsubscribeFromCommunity: - action = () => widget.outerContext.read().add(CommunityActionEvent( - communityAction: CommunityAction.follow, - communityId: widget.postViewMedia.postView.community.id, - value: false, - )); - break; - case PostCardAction.delete: - action = () => widget.outerContext - .read() - .add(FeedItemActionedEvent(postAction: PostAction.delete, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.post.deleted)); - break; - case PostCardAction.moderatorActions: - action = () => setState(() => page = PostActionBottomSheetPage.moderator); - pop = false; - break; - case PostCardAction.moderatorLockPost: - action = () => widget.outerContext - .read() - .add(FeedItemActionedEvent(postAction: PostAction.lock, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.post.locked)); - break; - case PostCardAction.moderatorPinCommunity: - action = () => widget.outerContext - .read() - .add(FeedItemActionedEvent(postAction: PostAction.pinCommunity, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.post.featuredCommunity)); - break; - case PostCardAction.moderatorRemovePost: - action = () => showRemovePostReasonBottomSheet(widget.outerContext, widget.postViewMedia); - break; - } - - if (pop) { - Navigator.of(context).pop(); - } - - action(); - } - - FutureOr _handleBack(bool stopDefaultButtonEvent, RouteInfo routeInfo) { - if ((page ?? widget.page) != PostActionBottomSheetPage.general) { - setState(() => page = PostActionBottomSheetPage.general); - return true; - } - - return false; - } -} - -void onTapCommunityName(BuildContext context, int communityId) { - navigateToFeedPage(context, feedType: FeedType.community, communityId: communityId); -} - -void showRemovePostReasonBottomSheet(BuildContext context, PostViewMedia postViewMedia) { - showModalBottomSheet( - context: context, - showDragHandle: true, - isScrollControlled: true, - builder: (_) => ReasonBottomSheet( - title: postViewMedia.postView.post.removed ? l10n.restorePost : l10n.removalReason, - submitLabel: postViewMedia.postView.post.removed ? l10n.restore : l10n.remove, - textHint: l10n.reason, - onSubmit: (String message) { - context.read().add( - FeedItemActionedEvent( - postAction: PostAction.remove, - postId: postViewMedia.postView.post.id, - value: { - 'remove': !postViewMedia.postView.post.removed, - 'reason': message, - }, - ), - ); - Navigator.of(context).pop(); - }, - ), - ); -} - -bool areCommunityAndUserOnSameInstance(PostView postView) { - String? communityInstance = fetchInstanceNameFromUrl(postView.community.actorId); - String? userInstance = fetchInstanceNameFromUrl(postView.creator.actorId); - return communityInstance == userInstance; -} diff --git a/lib/community/widgets/post_card.dart b/lib/community/widgets/post_card.dart index 0c5c0055d..b8541a8d9 100644 --- a/lib/community/widgets/post_card.dart +++ b/lib/community/widgets/post_card.dart @@ -5,7 +5,7 @@ import 'package:lemmy_api_client/v3.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/community/utils/post_actions.dart'; -import 'package:thunder/community/utils/post_card_action_helpers.dart'; +import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/community/widgets/post_card_view_comfortable.dart'; import 'package:thunder/community/widgets/post_card_view_compact.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; @@ -263,9 +263,9 @@ class _PostCardState extends State { onLongPress: () => showPostActionBottomModalSheet( context, widget.postViewMedia, - onBlockedUser: (userId) => context.read().add(FeedDismissBlockedEvent(userId: userId)), - onBlockedCommunity: (communityId) => context.read().add(FeedDismissBlockedEvent(communityId: communityId)), - onPostHidden: (postId) => context.read().add(FeedDismissHiddenPostEvent(postId: postId)), + // onBlockedUser: (userId) => context.read().add(FeedDismissBlockedEvent(userId: userId)), + // onBlockedCommunity: (communityId) => context.read().add(FeedDismissBlockedEvent(communityId: communityId)), + // onPostHidden: (postId) => context.read().add(FeedDismissHiddenPostEvent(postId: postId)), ), onTap: () async { PostView postView = widget.postViewMedia.postView; diff --git a/lib/community/widgets/post_card_view_comfortable.dart b/lib/community/widgets/post_card_view_comfortable.dart index 8875f97b2..b52142638 100644 --- a/lib/community/widgets/post_card_view_comfortable.dart +++ b/lib/community/widgets/post_card_view_comfortable.dart @@ -8,7 +8,7 @@ import 'package:lemmy_api_client/v3.dart'; import 'package:markdown/markdown.dart' hide Text; import 'package:thunder/account/bloc/account_bloc.dart'; -import 'package:thunder/community/utils/post_card_action_helpers.dart'; +import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/community/widgets/post_card_actions.dart'; import 'package:thunder/community/widgets/post_card_metadata.dart'; import 'package:thunder/core/enums/font_scale.dart'; @@ -348,9 +348,9 @@ class PostCardViewComfortable extends StatelessWidget { showPostActionBottomModalSheet( context, postViewMedia, - onBlockedUser: (userId) => context.read().add(FeedDismissBlockedEvent(userId: userId)), - onBlockedCommunity: (communityId) => context.read().add(FeedDismissBlockedEvent(communityId: communityId)), - onPostHidden: (postId) => context.read().add(FeedDismissHiddenPostEvent(postId: postId)), + // onBlockedUser: (userId) => context.read().add(FeedDismissBlockedEvent(userId: userId)), + // onBlockedCommunity: (communityId) => context.read().add(FeedDismissBlockedEvent(communityId: communityId)), + // onPostHidden: (postId) => context.read().add(FeedDismissHiddenPostEvent(postId: postId)), ); HapticFeedback.mediumImpact(); }), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c5bbe0fa4..435dbe82c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2251,6 +2251,10 @@ }, "subscriptions": "Subscriptions", "@subscriptions": {}, + "successfullyBannedUser": "Banned {username}", + "@successfullyBannedUser": { + "description": "Notification for successfully banning a user" + }, "successfullyBlocked": "Blocked.", "@successfullyBlocked": {}, "successfullyBlockedCommunity": "Blocked {communityName}", @@ -2259,6 +2263,10 @@ "@successfullyBlockedUser": { "description": "Notification for successfully blocking a user" }, + "successfullyUnbannedUser": "Unbanned {username}", + "@successfullyUnbannedUser": { + "description": "Notification for successfully unbanning a user" + }, "successfullyUnblocked": "Unblocked.", "@successfullyUnblocked": {}, "successfullyUnblockedCommunity": "Unblocked {communityName}", diff --git a/lib/post/widgets/community_post_action_bottom_sheet.dart b/lib/post/widgets/community_post_action_bottom_sheet.dart new file mode 100644 index 000000000..4c3c7a2d6 --- /dev/null +++ b/lib/post/widgets/community_post_action_bottom_sheet.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lemmy_api_client/v3.dart'; + +import 'package:thunder/community/bloc/community_bloc.dart'; +import 'package:thunder/community/enums/community_action.dart'; +import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/feed/feed.dart'; +import 'package:thunder/post/enums/post_action.dart'; +import 'package:thunder/shared/bottom_sheet_action.dart'; +import 'package:thunder/shared/divider.dart'; +import 'package:thunder/thunder/thunder_icons.dart'; + +/// Defines the actions that can be taken on a community +enum CommunityPostAction { + viewCommunity(icon: Icons.person, permissionType: PermissionType.user, requiresAuthentication: false), + subscribeToCommunity(icon: Icons.add_circle_outline_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + unsubscribeFromCommunity(icon: Icons.remove_circle_outline_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + blockCommunity(icon: Icons.block, permissionType: PermissionType.user, requiresAuthentication: true), + unblockCommunity(icon: Icons.block, permissionType: PermissionType.user, requiresAuthentication: true), + ; + + String get name => switch (this) { + CommunityPostAction.viewCommunity => "View Community", + CommunityPostAction.subscribeToCommunity => "Subscribe To Community", + CommunityPostAction.unsubscribeFromCommunity => "Unsubscribe From Community", + CommunityPostAction.blockCommunity => "Block Community", + CommunityPostAction.unblockCommunity => "Unblock Community", + }; + + /// The icon to use for the action + final IconData icon; + + /// The permission type to use for the action + final PermissionType permissionType; + + /// Whether or not the action requires user authentication + final bool requiresAuthentication; + + const CommunityPostAction({required this.icon, required this.permissionType, required this.requiresAuthentication}); +} + +/// A bottom sheet that allows the user to perform actions on a community. +/// +/// Given a [postViewMedia] and a [onAction] callback, this widget will display a list of actions that can be taken on the community. +/// The [onAction] callback will be triggered when an action is performed. This is useful if the parent widget requires an updated [CommunityView]. +class CommunityPostActionBottomSheet extends StatefulWidget { + const CommunityPostActionBottomSheet({super.key, required this.postViewMedia, required this.onAction}); + + /// The post information + final PostViewMedia postViewMedia; + + /// Called when an action is selected + final Function(CommunityView? communityView) onAction; + + @override + State createState() => _CommunityPostActionBottomSheetState(); +} + +class _CommunityPostActionBottomSheetState extends State { + void performAction(CommunityPostAction action) { + switch (action) { + case CommunityPostAction.viewCommunity: + context.pop(); + navigateToFeedPage(context, feedType: FeedType.community, communityId: widget.postViewMedia.postView.community.id); + break; + case CommunityPostAction.subscribeToCommunity: + context.read().add(CommunityActionEvent(communityId: widget.postViewMedia.postView.community.id, communityAction: CommunityAction.follow, value: true)); + break; + case CommunityPostAction.unsubscribeFromCommunity: + context.read().add(CommunityActionEvent(communityId: widget.postViewMedia.postView.community.id, communityAction: CommunityAction.follow, value: false)); + break; + case CommunityPostAction.blockCommunity: + context.read().add(CommunityActionEvent(communityId: widget.postViewMedia.postView.community.id, communityAction: CommunityAction.block, value: true)); + break; + case CommunityPostAction.unblockCommunity: + context.read().add(CommunityActionEvent(communityId: widget.postViewMedia.postView.community.id, communityAction: CommunityAction.block, value: false)); + break; + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final authState = context.read().state; + + List userActions = CommunityPostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); + List moderatorActions = CommunityPostAction.values.where((element) => element.permissionType == PermissionType.moderator).toList(); + List adminActions = CommunityPostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); + + final account = authState.getSiteResponse?.myUser?.localUserView.person; + final isModerator = authState.getSiteResponse?.myUser?.moderates.where((communityModeratorView) => communityModeratorView.moderator.actorId == account?.actorId).isNotEmpty ?? false; + final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; + + final isLoggedIn = authState.isLoggedIn; + final blockedCommunities = authState.getSiteResponse?.myUser?.communityBlocks ?? []; + final subscribedCommunities = authState.getSiteResponse?.myUser?.follows ?? []; + + final isCommunityBlocked = blockedCommunities.where((cbv) => cbv.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty; + final isSubscribedToCommunity = subscribedCommunities.where((cfv) => cfv.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty; + + if (!isLoggedIn) { + userActions = userActions.where((action) => action.requiresAuthentication == false).toList(); + } else { + if (isSubscribedToCommunity) { + userActions = userActions.where((action) => action != CommunityPostAction.subscribeToCommunity).toList(); + } else { + userActions = userActions.where((action) => action != CommunityPostAction.unsubscribeFromCommunity).toList(); + } + + if (isCommunityBlocked) { + userActions = userActions.where((action) => action != CommunityPostAction.blockCommunity).toList(); + } else { + userActions = userActions.where((action) => action != CommunityPostAction.unblockCommunity).toList(); + } + } + + return BlocListener( + listener: (context, state) { + if (state.status == CommunityStatus.success) { + context.pop(); + widget.onAction(state.communityView); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...userActions + .map( + (communityPostAction) => BottomSheetAction( + leading: Icon(communityPostAction.icon), + title: communityPostAction.name, + onTap: () => performAction(communityPostAction), + ), + ) + .toList() as List, + if (isModerator && moderatorActions.isNotEmpty) ...[ + const ThunderDivider(sliver: false, padding: false), + ...moderatorActions + .map( + (communityPostAction) => BottomSheetAction( + leading: Icon(communityPostAction.icon), + trailing: Padding( + padding: const EdgeInsets.only(left: 1), + child: Icon( + Thunder.shield, + size: 20, + color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.green), + ), + ), + title: communityPostAction.name, + onTap: () => performAction(communityPostAction), + ), + ) + .toList() as List, + ], + if (isAdmin && adminActions.isNotEmpty) ...[ + const ThunderDivider(sliver: false, padding: false), + ...adminActions + .map( + (communityPostAction) => BottomSheetAction( + leading: Icon(communityPostAction.icon), + trailing: Padding( + padding: const EdgeInsets.only(left: 1), + child: Icon( + Thunder.shield_crown, + size: 20, + color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), + ), + ), + title: communityPostAction.name, + onTap: () => performAction(communityPostAction), + ), + ) + .toList() as List, + ], + ], + ), + ); + } +} diff --git a/lib/post/widgets/general_post_action_bottom_sheet.dart b/lib/post/widgets/general_post_action_bottom_sheet.dart new file mode 100644 index 000000000..464dface6 --- /dev/null +++ b/lib/post/widgets/general_post_action_bottom_sheet.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import 'package:thunder/core/enums/full_name.dart'; +import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; +import 'package:thunder/shared/bottom_sheet_action.dart'; +import 'package:thunder/utils/instance.dart'; + +/// Defines the general actions that can be taken on a post +enum GeneralPostAction { + general(icon: Icons.more_horiz), + user(icon: Icons.person), + community(icon: Icons.group), + instance(icon: Icons.language), + share(icon: Icons.share); + + String get name => switch (this) { + GeneralPostAction.user => l10n.user, + GeneralPostAction.community => l10n.community, + GeneralPostAction.instance => l10n.instance(1), + GeneralPostAction.share => l10n.share, + GeneralPostAction.general => l10n.actions, + }; + + /// The title to use for the action. This is shown when the given page is active + String get title => switch (this) { + GeneralPostAction.user => l10n.userActions, + GeneralPostAction.community => l10n.communityActions, + GeneralPostAction.instance => l10n.instanceActions, + GeneralPostAction.share => l10n.share, + GeneralPostAction.general => l10n.actions, + }; + + /// The icon to use for the action + final IconData icon; + + const GeneralPostAction({required this.icon}); +} + +/// Defines the general top-levelactions that can be taken on a post. +/// Given a [postViewMedia] and a [onSwitchActivePage] callback, this widget will display a list of actions that can be taken on the post. +class GeneralPostActionBottomSheetPage extends StatefulWidget { + const GeneralPostActionBottomSheetPage({super.key, required this.postViewMedia, required this.onSwitchActivePage}); + + /// The post information + final PostViewMedia postViewMedia; + + /// Called when the active page is changed + final Function(GeneralPostAction page) onSwitchActivePage; + + @override + State createState() => _GeneralPostActionBottomSheetPageState(); +} + +class _GeneralPostActionBottomSheetPageState extends State { + String generateSubtitle(GeneralPostAction page) { + PostViewMedia postViewMedia = widget.postViewMedia; + + switch (page) { + case GeneralPostAction.user: + return generateUserFullName(context, postViewMedia.postView.creator.name, postViewMedia.postView.creator.displayName, fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId)); + case GeneralPostAction.community: + return generateCommunityFullName(context, postViewMedia.postView.community.name, postViewMedia.postView.community.title, fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId)); + case GeneralPostAction.instance: + return fetchInstanceNameFromUrl(postViewMedia.postView.post.apId) ?? ''; + case GeneralPostAction.share: + return l10n.share; + default: + return ''; + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: GeneralPostAction.values + .where((page) => page != GeneralPostAction.general) + .map( + (page) => BottomSheetAction( + leading: Icon(page.icon), + trailing: const Icon(Icons.chevron_right_rounded), + title: page.name, + subtitle: generateSubtitle(page), + onTap: () => widget.onSwitchActivePage(page), + ), + ) + .toList() as List, + ); + } +} diff --git a/lib/post/widgets/post_action_bottom_sheet.dart b/lib/post/widgets/post_action_bottom_sheet.dart new file mode 100644 index 000000000..e96aef9a5 --- /dev/null +++ b/lib/post/widgets/post_action_bottom_sheet.dart @@ -0,0 +1,977 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:lemmy_api_client/v3.dart'; + +import 'package:thunder/core/enums/full_name.dart'; +import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/post/enums/post_action.dart'; +import 'package:thunder/post/widgets/community_post_action_bottom_sheet.dart'; +import 'package:thunder/post/widgets/general_post_action_bottom_sheet.dart'; +import 'package:thunder/post/widgets/user_post_action_bottom_sheet.dart'; +import 'package:thunder/utils/instance.dart'; +import 'package:thunder/utils/global_context.dart'; + +final l10n = AppLocalizations.of(GlobalContext.context)!; + +// Defines the actions that can be taken on a post +enum PostPostAction { + upvote, + downvote, + save, + toggleRead, + hide, + share, + delete, + moderatorLockPost, + moderatorPinPost, + moderatorRemovePost, +} + +// Defines the actions that can be taken on an instance +enum InstancePostAction { + visitCommunityInstance, + blockCommunityInstance, + visitUserInstance, + blockUserInstance, +} + +// Defines the actions that can be taken when sharing a post +enum SharePostAction { + sharePost, + sharePostLocal, + shareImage, + shareMedia, + shareLink, + shareAdvanced, +} + +/// Programatically show the post action bottom sheet +void showPostActionBottomModalSheet( + BuildContext context, + PostViewMedia postViewMedia, { + GeneralPostAction page = GeneralPostAction.general, + void Function({PostAction? action})? onAction, +}) { + showModalBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + builder: (_) => PostActionBottomSheet(postViewMedia: postViewMedia), + ); +} + +class PostActionBottomSheet extends StatefulWidget { + const PostActionBottomSheet({super.key, required this.postViewMedia, this.initialPage = GeneralPostAction.general}); + + /// The post that is being acted on + final PostViewMedia postViewMedia; + + /// The initial page of the bottom sheet + final GeneralPostAction initialPage; + + @override + State createState() => _PostActionBottomSheetState(); +} + +class _PostActionBottomSheetState extends State { + GeneralPostAction currentPage = GeneralPostAction.general; + + @override + void initState() { + super.initState(); + currentPage = widget.initialPage; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + Widget actions = switch (currentPage) { + GeneralPostAction.general => GeneralPostActionBottomSheetPage( + postViewMedia: widget.postViewMedia, + onSwitchActivePage: (page) => setState(() => currentPage = page), + ), + GeneralPostAction.user => UserPostActionBottomSheet( + postViewMedia: widget.postViewMedia, + onAction: (PersonView? updatedPersonView) {}, + ), + GeneralPostAction.community => CommunityPostActionBottomSheet( + postViewMedia: widget.postViewMedia, + onAction: (CommunityView? updatedCommunityView) {}, + ), + GeneralPostAction.instance => Container(), + GeneralPostAction.share => Container(), + }; + + return SafeArea( + child: AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutCubicEmphasized, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + currentPage != GeneralPostAction.general + ? IconButton(onPressed: () => setState(() => currentPage = GeneralPostAction.general), icon: const Icon(Icons.chevron_left_rounded)) + : const SizedBox(width: 12.0), + Wrap( + direction: Axis.vertical, + children: [ + Text(currentPage.title, style: theme.textTheme.titleLarge), + if (currentPage == GeneralPostAction.user) + Text( + generateUserFullName( + context, + widget.postViewMedia.postView.creator.name, + widget.postViewMedia.postView.creator.displayName, + fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId), + ), + ), + if (currentPage == GeneralPostAction.community) + Text( + generateCommunityFullName( + context, + widget.postViewMedia.postView.community.name, + widget.postViewMedia.postView.community.title, + fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId), + ), + ), + if (currentPage == GeneralPostAction.instance) Text(fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId) ?? ''), + ], + ), + ], + ), + const SizedBox(height: 16.0), + actions, + ], + ), + ), + ), + ); + } +} + + + + + + +// class PostActionBottomSheet { +// const PostActionBottomSheet({ +// required this.postCardAction, +// required this.icon, +// this.trailingIcon, +// required this.label, +// this.getColor, +// this.getForegroundColor, +// this.getOverrideIcon, +// this.getOverrideLabel, +// this.getSubtitleLabel, +// this.shouldShow, +// this.shouldEnable, +// }); + +// final PostCardAction postCardAction; +// final IconData icon; +// final IconData? trailingIcon; +// final String label; +// final Color Function(BuildContext context)? getColor; +// final Color? Function(BuildContext context, PostView postView)? getForegroundColor; +// final IconData? Function(PostView postView)? getOverrideIcon; +// final String? Function(BuildContext context, PostView postView)? getOverrideLabel; +// final String? Function(BuildContext context, PostViewMedia postViewMedia)? getSubtitleLabel; +// final bool Function(BuildContext context, PostView commentView)? shouldShow; +// final bool Function(bool isUserLoggedIn)? shouldEnable; +// } + +// final l10n = AppLocalizations.of(GlobalContext.context)!; + +// final List postCardActionItems = [ +// PostActionBottomSheet( +// postCardAction: PostCardAction.userActions, +// icon: Icons.person_rounded, +// label: l10n.user, +// getSubtitleLabel: (context, postViewMedia) => generateUserFullName( +// context, +// postViewMedia.postView.creator.name, +// postViewMedia.postView.creator.displayName, +// fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId), +// ), +// trailingIcon: Icons.chevron_right_rounded, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.visitProfile, +// icon: Icons.person_search_rounded, +// label: l10n.visitUserProfile, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.blockUser, +// icon: Icons.block, +// label: l10n.blockUser, +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.communityActions, +// icon: Icons.people_rounded, +// label: l10n.community, +// getSubtitleLabel: (context, postViewMedia) => generateCommunityFullName( +// context, +// postViewMedia.postView.community.name, +// postViewMedia.postView.community.title, +// fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), +// ), +// trailingIcon: Icons.chevron_right_rounded, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.visitCommunity, +// icon: Icons.home_work_rounded, +// label: l10n.visitCommunity, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.subscribeToCommunity, +// icon: Icons.add_circle_outline_rounded, +// label: l10n.subscribeToCommunity, +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.unsubscribeFromCommunity, +// icon: Icons.remove_circle_outline_rounded, +// label: l10n.unsubscribeFromCommunity, +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.blockCommunity, +// icon: Icons.block_rounded, +// label: l10n.blockCommunity, +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.instanceActions, +// icon: Icons.language_rounded, +// label: l10n.instance(1), +// getSubtitleLabel: (context, postViewMedia) { +// return areCommunityAndUserOnSameInstance(postViewMedia.postView) +// ? fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId) +// : '${fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId)} • ${fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId)}'; +// }, +// trailingIcon: Icons.chevron_right_rounded, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.visitCommunityInstance, +// icon: Icons.language, +// label: '', +// getOverrideLabel: (context, postView) { +// return areCommunityAndUserOnSameInstance(postView) ? l10n.visitInstance : l10n.visitCommunityInstance; +// }, +// getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.blockCommunityInstance, +// icon: Icons.block_rounded, +// label: '', +// getOverrideLabel: (context, postView) { +// return areCommunityAndUserOnSameInstance(postView) ? l10n.blockInstance : l10n.blockCommunityInstance; +// }, +// getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.visitUserInstance, +// icon: Icons.language, +// label: l10n.visitUserInstance, +// getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId), +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.blockUserInstance, +// icon: Icons.block_rounded, +// label: l10n.blockUserInstance, +// getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId), +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.sharePost, +// icon: Icons.share_rounded, +// label: l10n.sharePost, +// getSubtitleLabel: (context, postViewMedia) => postViewMedia.postView.post.apId, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.sharePostLocal, +// icon: Icons.share_rounded, +// label: l10n.sharePostLocal, +// getSubtitleLabel: (context, postViewMedia) => LemmyClient.instance.generatePostUrl(postViewMedia.postView.post.id), +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.shareImage, +// icon: Icons.image_rounded, +// label: l10n.shareImage, +// getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.imageUrl, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.shareMedia, +// icon: Icons.personal_video_rounded, +// label: l10n.shareMediaLink, +// getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.mediaUrl, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.shareLink, +// icon: Icons.link_rounded, +// label: l10n.shareLink, +// getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.originalUrl, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.shareAdvanced, +// icon: Icons.screen_share_rounded, +// label: l10n.advanced, +// getSubtitleLabel: (context, postViewMedia) => l10n.useAdvancedShareSheet, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.upvote, +// label: l10n.upvote, +// icon: Icons.arrow_upward_rounded, +// getColor: (context) => context.read().state.upvoteColor.color, +// getForegroundColor: (context, postView) => postView.myVote == 1 ? context.read().state.upvoteColor.color : null, +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.downvote, +// label: l10n.downvote, +// icon: Icons.arrow_downward_rounded, +// getColor: (context) => context.read().state.downvoteColor.color, +// getForegroundColor: (context, postView) => postView.myVote == -1 ? context.read().state.downvoteColor.color : null, +// shouldShow: (context, commentView) => context.read().state.downvotesEnabled, +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.save, +// label: l10n.save, +// icon: Icons.star_border_rounded, +// getColor: (context) => context.read().state.saveColor.color, +// getForegroundColor: (context, postView) => postView.saved ? context.read().state.saveColor.color : null, +// getOverrideIcon: (postView) => postView.saved ? Icons.star_rounded : null, +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.toggleRead, +// label: l10n.toggelRead, +// icon: Icons.mail_outline_outlined, +// getColor: (context) => context.read().state.markReadColor.color, +// getOverrideIcon: (postView) => postView.read ? Icons.mark_email_unread_rounded : Icons.mark_email_read_outlined, +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.hide, +// label: l10n.hide, +// getOverrideLabel: (context, postView) => postView.hidden == true ? l10n.unhide : l10n.hide, +// icon: Icons.visibility_off_rounded, +// getColor: (context) => context.read().state.hideColor.color, +// getOverrideIcon: (postView) => postView.hidden == true ? Icons.visibility_rounded : Icons.visibility_off_rounded, +// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.share, +// icon: Icons.share_rounded, +// label: l10n.share, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.delete, +// icon: Icons.delete_rounded, +// label: l10n.delete, +// getOverrideIcon: (postView) => postView.post.deleted ? Icons.restore_from_trash_rounded : Icons.delete_rounded, +// getOverrideLabel: (context, postView) => postView.post.deleted ? l10n.restore : l10n.delete, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.moderatorActions, +// icon: Icons.shield_rounded, +// trailingIcon: Icons.chevron_right_rounded, +// label: l10n.moderatorActions, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.moderatorLockPost, +// icon: Icons.lock, +// label: l10n.lockPost, +// getOverrideIcon: (postView) => postView.post.locked ? Icons.lock_open_rounded : Icons.lock, +// getOverrideLabel: (context, postView) => postView.post.locked ? l10n.unlockPost : l10n.lockPost, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.moderatorPinCommunity, +// icon: Icons.push_pin_rounded, +// label: l10n.pinToCommunity, +// getOverrideIcon: (postView) => postView.post.featuredCommunity ? Icons.push_pin_rounded : Icons.push_pin_outlined, +// getOverrideLabel: (context, postView) => postView.post.featuredCommunity ? l10n.unpinFromCommunity : l10n.pinToCommunity, +// ), +// PostActionBottomSheet( +// postCardAction: PostCardAction.moderatorRemovePost, +// icon: Icons.delete_forever_rounded, +// label: l10n.removePost, +// getOverrideIcon: (postView) => postView.post.removed ? Icons.restore_from_trash_rounded : Icons.delete_forever_rounded, +// getOverrideLabel: (context, postView) => postView.post.removed ? l10n.restorePost : l10n.removePost, +// ) +// ]; + +// enum PostActionBottomSheetPage { +// general, +// share, +// moderator, +// user, +// community, +// instance, +// } + +// void showPostActionBottomModalSheet( +// BuildContext context, +// PostViewMedia postViewMedia, { +// PostActionBottomSheetPage page = PostActionBottomSheetPage.general, +// void Function(int userId)? onBlockedUser, +// void Function(int userId)? onBlockedCommunity, +// void Function(int postId)? onPostHidden, +// }) { +// final bool isOwnPost = postViewMedia.postView.creator.id == context.read().state.account?.userId; +// final bool isModerator = +// context.read().state.moderates.any((CommunityModeratorView communityModeratorView) => communityModeratorView.community.id == postViewMedia.postView.community.id); +// final int? currentUserId = context.read().state.account?.userId; + +// // Generate the list of default actions for the general page +// final List defaultPostCardActions = postCardActionItems +// .where((extendedAction) => [ +// PostCardAction.userActions, +// PostCardAction.communityActions, +// PostCardAction.instanceActions, +// ].contains(extendedAction.postCardAction)) +// .toList(); + +// // Add the moderator actions submenu +// if (isModerator) { +// defaultPostCardActions.add(postCardActionItems.firstWhere((PostActionBottomSheet extendedPostCardActions) => extendedPostCardActions.postCardAction == PostCardAction.moderatorActions)); +// } + +// // Generate the list of default multi actions +// final List defaultMultiPostCardActions = postCardActionItems +// .where((extendedAction) => [ +// PostCardAction.upvote, +// PostCardAction.downvote, +// PostCardAction.save, +// PostCardAction.toggleRead, +// PostCardAction.hide, +// PostCardAction.share, +// if (isOwnPost) PostCardAction.delete, +// ].contains(extendedAction.postCardAction)) +// .toList(); + +// // Remove hide if unsupported +// if (defaultMultiPostCardActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.hide) && !LemmyClient.instance.supportsFeature(LemmyFeature.hidePosts)) { +// defaultMultiPostCardActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.hide); +// } + +// // Generate the list of moderator actions +// final List moderatorPostCardActions = postCardActionItems +// .where((extendedAction) => [ +// PostCardAction.moderatorLockPost, +// PostCardAction.moderatorPinCommunity, +// PostCardAction.moderatorRemovePost, +// ].contains(extendedAction.postCardAction)) +// .toList(); + +// // Generate the list of share actions +// final List sharePostCardActions = postCardActionItems +// .where((extendedAction) => [ +// PostCardAction.sharePost, +// PostCardAction.sharePostLocal, +// PostCardAction.shareImage, +// PostCardAction.shareMedia, +// PostCardAction.shareLink, +// PostCardAction.shareAdvanced, +// ].contains(extendedAction.postCardAction)) +// .toList(); + +// // Remove the share link option if there is no link +// // Or if the media link is the same as the external link +// if (postViewMedia.media.isEmpty || +// postViewMedia.media.first.mediaType == MediaType.text || +// postViewMedia.media.first.originalUrl == postViewMedia.media.first.imageUrl || +// postViewMedia.media.first.originalUrl == postViewMedia.media.first.mediaUrl) { +// sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareLink); +// } + +// // Remove the share image option if there is no image +// if (postViewMedia.media.isEmpty || postViewMedia.media.first.imageUrl?.isNotEmpty != true) { +// sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareImage); +// } + +// // Remove the share media option if there is no media +// if (postViewMedia.media.isEmpty || postViewMedia.media.first.mediaUrl?.isNotEmpty != true) { +// sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareMedia); +// } + +// // Remove the share local option if it is the same as the original +// if (postViewMedia.postView.post.apId == LemmyClient.instance.generatePostUrl(postViewMedia.postView.post.id)) { +// sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.sharePostLocal); +// } + +// // Generate the list of user actions +// final List userActions = postCardActionItems +// .where((extendedAction) => [ +// PostCardAction.visitProfile, +// if (postViewMedia.postView.creator.id != currentUserId) PostCardAction.blockUser, +// ].contains(extendedAction.postCardAction)) +// .toList(); + +// // Generate the list of community actions +// final List communityActions = postCardActionItems +// .where((extendedAction) => [ +// PostCardAction.visitCommunity, +// postViewMedia.postView.subscribed == SubscribedType.notSubscribed ? PostCardAction.subscribeToCommunity : PostCardAction.unsubscribeFromCommunity, +// PostCardAction.blockCommunity, +// ].contains(extendedAction.postCardAction)) +// .toList(); + +// // Hide the option to block a community if the user is subscribed to it +// if (communityActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockCommunity) && postViewMedia.postView.subscribed != SubscribedType.notSubscribed) { +// communityActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockCommunity); +// } + +// // Generate the list of instance actions +// final List instanceActions = postCardActionItems +// .where((extendedAction) => [ +// PostCardAction.visitCommunityInstance, +// PostCardAction.blockCommunityInstance, +// PostCardAction.visitUserInstance, +// PostCardAction.blockUserInstance, +// ].contains(extendedAction.postCardAction)) +// .toList(); + +// // Remove block if unsupported +// if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockCommunityInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { +// instanceActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockCommunityInstance); +// } +// if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockUserInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { +// instanceActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockUserInstance); +// } + +// // Hide user block if user's instance is the same as the community' sinstance +// bool areSameInstance = areCommunityAndUserOnSameInstance(postViewMedia.postView); +// if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.visitUserInstance) && areSameInstance) { +// instanceActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.visitUserInstance); +// } +// if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockUserInstance) && areSameInstance) { +// instanceActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockUserInstance); +// } + +// showModalBottomSheet( +// showDragHandle: true, +// isScrollControlled: true, +// context: context, +// builder: (builderContext) => PostCardActionPicker( +// postViewMedia: postViewMedia, +// page: page, +// postCardActions: { +// PostActionBottomSheetPage.general: defaultPostCardActions, +// PostActionBottomSheetPage.moderator: moderatorPostCardActions, +// PostActionBottomSheetPage.share: sharePostCardActions, +// PostActionBottomSheetPage.user: userActions, +// PostActionBottomSheetPage.community: communityActions, +// PostActionBottomSheetPage.instance: instanceActions, +// }, +// multiPostCardActions: {PostActionBottomSheetPage.general: defaultMultiPostCardActions}, +// titles: { +// PostActionBottomSheetPage.general: l10n.actions, +// PostActionBottomSheetPage.moderator: l10n.moderatorActions, +// PostActionBottomSheetPage.share: l10n.share, +// PostActionBottomSheetPage.user: l10n.userActions, +// PostActionBottomSheetPage.community: l10n.communityActions, +// PostActionBottomSheetPage.instance: l10n.instanceActions, +// }, +// outerContext: context, +// onBlockedUser: onBlockedUser, +// onBlockedCommunity: onBlockedCommunity, +// onPostHidden: onPostHidden, +// ), +// ); +// } + +// class PostCardActionPicker extends StatefulWidget { +// /// The post +// final PostViewMedia postViewMedia; + +// /// This is the list of quick actions that are shown horizontally across the top of the sheet +// final Map> multiPostCardActions; + +// /// This is the set of full actions to display vertically in a list +// final Map> postCardActions; + +// /// This is the set of titles to show for each page +// final Map titles; + +// /// The current page +// final PostActionBottomSheetPage page; + +// /// The context from whoever invoked this sheet (useful for blocs that would otherwise be missing) +// final BuildContext outerContext; + +// /// Callback used to notify that we blocked a user +// final void Function(int userId)? onBlockedUser; + +// /// Callback used to notify that we blocked a community +// final Function(int userId)? onBlockedCommunity; + +// /// Callback used to notify that we hid a post +// final Function(int postId)? onPostHidden; + +// const PostCardActionPicker({ +// super.key, +// required this.postViewMedia, +// required this.page, +// required this.postCardActions, +// required this.multiPostCardActions, +// required this.titles, +// required this.outerContext, +// required this.onBlockedUser, +// required this.onBlockedCommunity, +// required this.onPostHidden, +// }); + +// @override +// State createState() => _PostCardActionPickerState(); +// } + +// class _PostCardActionPickerState extends State { +// PostActionBottomSheetPage? page; + +// @override +// void initState() { +// super.initState(); + +// BackButtonInterceptor.add(_handleBack); +// } + +// @override +// void dispose() { +// BackButtonInterceptor.remove(_handleBack); + +// super.dispose(); +// } + +// @override +// Widget build(BuildContext context) { +// final ThemeData theme = Theme.of(context); +// final bool isUserLoggedIn = context.read().state.isLoggedIn; + +// return SingleChildScrollView( +// child: AnimatedSize( +// duration: const Duration(milliseconds: 100), +// curve: Curves.easeInOut, +// child: SingleChildScrollView( +// child: Column( +// mainAxisAlignment: MainAxisAlignment.start, +// mainAxisSize: MainAxisSize.max, +// children: [ +// Semantics( +// label: '${widget.titles[page ?? widget.page] ?? l10n.actions}, ${(page ?? widget.page) == PostActionBottomSheetPage.general ? '' : l10n.backButton}', +// child: Padding( +// padding: const EdgeInsets.only(left: 10, right: 10), +// child: Material( +// borderRadius: BorderRadius.circular(50), +// color: Colors.transparent, +// child: InkWell( +// borderRadius: BorderRadius.circular(50), +// onTap: (page ?? widget.page) == PostActionBottomSheetPage.general ? null : () => setState(() => page = PostActionBottomSheetPage.general), +// child: Padding( +// padding: const EdgeInsets.fromLTRB(12.0, 10, 16.0, 10.0), +// child: Align( +// alignment: Alignment.centerLeft, +// child: Row( +// children: [ +// if ((page ?? widget.page) != PostActionBottomSheetPage.general) ...[ +// const Icon(Icons.chevron_left, size: 30), +// const SizedBox(width: 12), +// ], +// Semantics( +// excludeSemantics: true, +// child: Text( +// widget.titles[page ?? widget.page] ?? l10n.actions, +// style: theme.textTheme.titleLarge, +// ), +// ), +// ], +// ), +// ), +// ), +// ), +// ), +// ), +// ), +// // Post metadata chips +// if ((page ?? PostActionBottomSheetPage.general) == PostActionBottomSheetPage.general) +// Row( +// children: [ +// const SizedBox(width: 20), +// LanguagePostCardMetaData(languageId: widget.postViewMedia.postView.post.languageId), +// ], +// ), +// if (widget.multiPostCardActions[page ?? widget.page]?.isNotEmpty == true) +// MultiPickerItem( +// pickerItems: [ +// ...widget.multiPostCardActions[page ?? widget.page]!.where((a) => a.shouldShow?.call(context, widget.postViewMedia.postView) ?? true).map( +// (a) { +// return PickerItemData( +// label: a.getOverrideLabel?.call(context, widget.postViewMedia.postView) ?? a.label, +// icon: a.getOverrideIcon?.call(widget.postViewMedia.postView) ?? a.icon, +// backgroundColor: a.getColor?.call(context), +// foregroundColor: a.getForegroundColor?.call(context, widget.postViewMedia.postView), +// onSelected: (a.shouldEnable?.call(isUserLoggedIn) ?? true) ? () => onSelected(a.postCardAction) : null, +// ); +// }, +// ), +// ], +// ), +// if (widget.postCardActions[page ?? widget.page]?.isNotEmpty == true) +// ListView.builder( +// shrinkWrap: true, +// physics: const NeverScrollableScrollPhysics(), +// itemCount: widget.postCardActions[page ?? widget.page]!.length, +// itemBuilder: (BuildContext itemBuilderContext, int index) { +// return PickerItem( +// label: widget.postCardActions[page ?? widget.page]![index].getOverrideLabel?.call(context, widget.postViewMedia.postView) ?? +// widget.postCardActions[page ?? widget.page]![index].label, +// subtitle: widget.postCardActions[page ?? widget.page]![index].getSubtitleLabel?.call(context, widget.postViewMedia), +// icon: widget.postCardActions[page ?? widget.page]![index].getOverrideIcon?.call(widget.postViewMedia.postView) ?? widget.postCardActions[page ?? widget.page]![index].icon, +// trailingIcon: widget.postCardActions[page ?? widget.page]![index].trailingIcon, +// onSelected: (widget.postCardActions[page ?? widget.page]![index].shouldEnable?.call(isUserLoggedIn) ?? true) +// ? () => onSelected(widget.postCardActions[page ?? widget.page]![index].postCardAction) +// : null, +// ); +// }, +// ), +// const SizedBox(height: 16.0), +// ], +// ), +// ), +// ), +// ); +// } + +// void onSelected(PostCardAction postCardAction) async { +// bool pop = true; +// void Function() action; + +// switch (postCardAction) { +// case PostCardAction.visitCommunity: +// action = () => onTapCommunityName(widget.outerContext, widget.postViewMedia.postView.community.id); +// break; +// case PostCardAction.userActions: +// action = () => setState(() => page = PostActionBottomSheetPage.user); +// pop = false; +// break; +// case PostCardAction.visitProfile: +// action = () => navigateToFeedPage(widget.outerContext, feedType: FeedType.user, userId: widget.postViewMedia.postView.post.creatorId); +// break; +// case PostCardAction.visitCommunityInstance: +// action = () => navigateToInstancePage(widget.outerContext, +// instanceHost: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId)!, instanceId: widget.postViewMedia.postView.community.instanceId); +// break; +// case PostCardAction.visitUserInstance: +// action = () => navigateToInstancePage(widget.outerContext, +// instanceHost: fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId)!, instanceId: widget.postViewMedia.postView.creator.instanceId); +// break; +// case PostCardAction.sharePost: +// action = () => Share.share(widget.postViewMedia.postView.post.apId); +// break; +// case PostCardAction.sharePostLocal: +// action = () => Share.share(LemmyClient.instance.generatePostUrl(widget.postViewMedia.postView.post.id)); +// break; +// case PostCardAction.shareImage: +// action = () async { +// if (widget.postViewMedia.media.first.imageUrl != null) { +// try { +// // Try to get the cached image first +// var media = await DefaultCacheManager().getFileFromCache(widget.postViewMedia.media.first.imageUrl!); +// File? mediaFile = media?.file; + +// if (media == null) { +// // Tell user we're downloading the image +// showSnackbar(AppLocalizations.of(widget.outerContext)!.downloadingMedia); + +// // Download +// mediaFile = await DefaultCacheManager().getSingleFile(widget.postViewMedia.media.first.imageUrl!); +// } + +// // Share +// await Share.shareXFiles([XFile(mediaFile!.path)]); +// } catch (e) { +// // Tell the user that the download failed +// showSnackbar(AppLocalizations.of(widget.outerContext)!.errorDownloadingMedia(e)); +// } +// } +// }; +// break; +// case PostCardAction.shareMedia: +// action = () => Share.share(widget.postViewMedia.media.first.mediaUrl!); +// break; +// case PostCardAction.shareLink: +// action = () { +// if (widget.postViewMedia.media.first.originalUrl != null) Share.share(widget.postViewMedia.media.first.originalUrl!); +// }; +// break; +// case PostCardAction.shareAdvanced: +// action = () => showAdvancedShareSheet(widget.outerContext, widget.postViewMedia); +// break; +// case PostCardAction.instanceActions: +// action = () => setState(() => page = PostActionBottomSheetPage.instance); +// pop = false; +// break; +// case PostCardAction.blockCommunityInstance: +// action = () => widget.outerContext.read().add(InstanceActionEvent( +// instanceAction: InstanceAction.block, +// instanceId: widget.postViewMedia.postView.community.instanceId, +// domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId), +// value: true, +// )); +// break; +// case PostCardAction.blockUserInstance: +// action = () => widget.outerContext.read().add(InstanceActionEvent( +// instanceAction: InstanceAction.block, +// instanceId: widget.postViewMedia.postView.creator.instanceId, +// domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId), +// value: true, +// )); +// break; +// case PostCardAction.communityActions: +// action = () => setState(() => page = PostActionBottomSheetPage.community); +// pop = false; +// break; +// case PostCardAction.blockCommunity: +// action = () { +// widget.outerContext.read().add(CommunityActionEvent(communityAction: CommunityAction.block, communityId: widget.postViewMedia.postView.community.id, value: true)); +// widget.onBlockedCommunity?.call(widget.postViewMedia.postView.community.id); +// }; +// break; +// case PostCardAction.upvote: +// action = () => widget.outerContext +// .read() +// .add(FeedItemActionedEvent(postAction: PostAction.vote, postId: widget.postViewMedia.postView.post.id, value: widget.postViewMedia.postView.myVote == 1 ? 0 : 1)); +// break; +// case PostCardAction.downvote: +// action = () => widget.outerContext +// .read() +// .add(FeedItemActionedEvent(postAction: PostAction.vote, postId: widget.postViewMedia.postView.post.id, value: widget.postViewMedia.postView.myVote == -1 ? 0 : -1)); +// break; +// case PostCardAction.save: +// action = () => +// widget.outerContext.read().add(FeedItemActionedEvent(postAction: PostAction.save, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.saved)); +// break; +// case PostCardAction.toggleRead: +// action = () => +// widget.outerContext.read().add(FeedItemActionedEvent(postAction: PostAction.read, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.read)); +// break; +// case PostCardAction.hide: +// action = () => widget.outerContext +// .read() +// .add(FeedItemActionedEvent(postAction: PostAction.hide, postId: widget.postViewMedia.postView.post.id, value: !(widget.postViewMedia.postView.hidden ?? false))); +// widget.onPostHidden?.call(widget.postViewMedia.postView.post.id); +// break; +// case PostCardAction.share: +// pop = false; +// action = () => setState(() => page = PostActionBottomSheetPage.share); +// break; +// case PostCardAction.blockUser: +// action = () { +// widget.outerContext.read().add(UserActionEvent(userAction: UserAction.block, userId: widget.postViewMedia.postView.creator.id, value: true)); +// widget.onBlockedCommunity?.call(widget.postViewMedia.postView.creator.id); +// }; +// break; +// case PostCardAction.subscribeToCommunity: +// action = () => widget.outerContext.read().add(CommunityActionEvent( +// communityAction: CommunityAction.follow, +// communityId: widget.postViewMedia.postView.community.id, +// value: true, +// )); +// break; +// case PostCardAction.unsubscribeFromCommunity: +// action = () => widget.outerContext.read().add(CommunityActionEvent( +// communityAction: CommunityAction.follow, +// communityId: widget.postViewMedia.postView.community.id, +// value: false, +// )); +// break; +// case PostCardAction.delete: +// action = () => widget.outerContext +// .read() +// .add(FeedItemActionedEvent(postAction: PostAction.delete, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.post.deleted)); +// break; +// case PostCardAction.moderatorActions: +// action = () => setState(() => page = PostActionBottomSheetPage.moderator); +// pop = false; +// break; +// case PostCardAction.moderatorLockPost: +// action = () => widget.outerContext +// .read() +// .add(FeedItemActionedEvent(postAction: PostAction.lock, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.post.locked)); +// break; +// case PostCardAction.moderatorPinCommunity: +// action = () => widget.outerContext +// .read() +// .add(FeedItemActionedEvent(postAction: PostAction.pinCommunity, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.post.featuredCommunity)); +// break; +// case PostCardAction.moderatorRemovePost: +// action = () => showRemovePostReasonBottomSheet(widget.outerContext, widget.postViewMedia); +// break; +// } + +// if (pop) { +// Navigator.of(context).pop(); +// } + +// action(); +// } + +// FutureOr _handleBack(bool stopDefaultButtonEvent, RouteInfo routeInfo) { +// if ((page ?? widget.page) != PostActionBottomSheetPage.general) { +// setState(() => page = PostActionBottomSheetPage.general); +// return true; +// } + +// return false; +// } +// } + +// void onTapCommunityName(BuildContext context, int communityId) { +// navigateToFeedPage(context, feedType: FeedType.community, communityId: communityId); +// } + +// void showRemovePostReasonBottomSheet(BuildContext context, PostViewMedia postViewMedia) { +// showModalBottomSheet( +// context: context, +// showDragHandle: true, +// isScrollControlled: true, +// builder: (_) => ReasonBottomSheet( +// title: postViewMedia.postView.post.removed ? l10n.restorePost : l10n.removalReason, +// submitLabel: postViewMedia.postView.post.removed ? l10n.restore : l10n.remove, +// textHint: l10n.reason, +// onSubmit: (String message) { +// context.read().add( +// FeedItemActionedEvent( +// postAction: PostAction.remove, +// postId: postViewMedia.postView.post.id, +// value: { +// 'remove': !postViewMedia.postView.post.removed, +// 'reason': message, +// }, +// ), +// ); +// Navigator.of(context).pop(); +// }, +// ), +// ); +// } + +// bool areCommunityAndUserOnSameInstance(PostView postView) { +// String? communityInstance = fetchInstanceNameFromUrl(postView.community.actorId); +// String? userInstance = fetchInstanceNameFromUrl(postView.creator.actorId); +// return communityInstance == userInstance; +// } diff --git a/lib/post/widgets/post_view.dart b/lib/post/widgets/post_view.dart index e4ca8cb51..01e52ff4a 100644 --- a/lib/post/widgets/post_view.dart +++ b/lib/post/widgets/post_view.dart @@ -18,7 +18,8 @@ import 'package:thunder/account/bloc/account_bloc.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/comment/utils/navigate_comment.dart'; import 'package:thunder/community/pages/create_post_page.dart'; -import 'package:thunder/community/utils/post_card_action_helpers.dart'; +import 'package:thunder/post/widgets/general_post_action_bottom_sheet.dart'; +import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/community/widgets/post_card_type_badge.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; @@ -298,7 +299,7 @@ class _PostSubviewState extends State with SingleTickerProviderStat showPostActionBottomModalSheet( context, widget.postViewMedia, - page: PostActionBottomSheetPage.share, + page: GeneralPostAction.share, ); }, onEdit: () async { diff --git a/lib/post/widgets/user_post_action_bottom_sheet.dart b/lib/post/widgets/user_post_action_bottom_sheet.dart new file mode 100644 index 000000000..4249491e5 --- /dev/null +++ b/lib/post/widgets/user_post_action_bottom_sheet.dart @@ -0,0 +1,237 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lemmy_api_client/v3.dart'; + +import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/feed/utils/utils.dart'; +import 'package:thunder/feed/view/feed_page.dart'; +import 'package:thunder/post/enums/post_action.dart'; +import 'package:thunder/post/utils/comment_action_helpers.dart'; +import 'package:thunder/shared/bottom_sheet_action.dart'; +import 'package:thunder/shared/divider.dart'; +import 'package:thunder/thunder/thunder_icons.dart'; +import 'package:thunder/user/bloc/user_bloc.dart'; +import 'package:thunder/user/enums/user_action.dart'; + +// Defines the actions that can be taken on a user +enum UserPostAction { + viewProfile(icon: Icons.person, permissionType: PermissionType.user, requiresAuthentication: false), + blockUser(icon: Icons.block, permissionType: PermissionType.user, requiresAuthentication: true), + unblockUser(icon: Icons.block, permissionType: PermissionType.user, requiresAuthentication: true), + banUserFromCommunity(icon: Icons.block, permissionType: PermissionType.moderator, requiresAuthentication: true), + unbanUserFromCommunity(icon: Icons.block, permissionType: PermissionType.moderator, requiresAuthentication: true), + addUserAsCommunityModerator(icon: Icons.person_add_rounded, permissionType: PermissionType.moderator, requiresAuthentication: true), + removeUserAsCommunityModerator(icon: Icons.person_remove_rounded, permissionType: PermissionType.moderator, requiresAuthentication: true), + // banUser(icon: Icons.block, permissionType: PermissionType.admin, requiresAuthentication: true), + // unbanUser(icon: Icons.block, permissionType: PermissionType.admin, requiresAuthentication: true), + // purgeUser(icon: Icons.delete_rounded, permissionType: PermissionType.admin, requiresAuthentication: true), + // addUserAsAdmin(icon: Icons.person_add_rounded, permissionType: PermissionType.admin, requiresAuthentication: true), + // removeUserAsAdmin(icon: Icons.person_remove_rounded, permissionType: PermissionType.admin, requiresAuthentication: true), + ; + + String get name => switch (this) { + UserPostAction.viewProfile => l10n.visitUserProfile, + UserPostAction.blockUser => l10n.blockUser, + UserPostAction.unblockUser => "Unblock User", + UserPostAction.banUserFromCommunity => "Ban From Community", + UserPostAction.unbanUserFromCommunity => "Unban From Community", + UserPostAction.addUserAsCommunityModerator => "Add As Community Moderator", + UserPostAction.removeUserAsCommunityModerator => "Remove As Community Moderator", + // UserPostAction.banUser => "Ban From Instance", + // UserPostAction.unbanUser => "Unban User From Instance", + // UserPostAction.purgeUser => "Purge User", + // UserPostAction.addUserAsAdmin => "Add As Admin", + // UserPostAction.removeUserAsAdmin => "Remove As Admin", + }; + + /// The icon to use for the action + final IconData icon; + + /// The permission type to use for the action + final PermissionType permissionType; + + /// Whether or not the action requires user authentication + final bool requiresAuthentication; + + const UserPostAction({required this.icon, required this.permissionType, required this.requiresAuthentication}); +} + +/// A bottom sheet that allows the user to perform actions on a user. +/// +/// Given a [postViewMedia] and a [onAction] callback, this widget will display a list of actions that can be taken on the user. +/// The [onAction] callback will be triggered when an action is performed. This is useful if the parent widget requires an updated [PersonView]. +class UserPostActionBottomSheet extends StatefulWidget { + const UserPostActionBottomSheet({super.key, required this.postViewMedia, required this.onAction}); + + /// The post information + final PostViewMedia postViewMedia; + + /// Called when an action is selected + final Function(PersonView? personView) onAction; + + @override + State createState() => _UserPostActionBottomSheetState(); +} + +class _UserPostActionBottomSheetState extends State { + void performAction(UserPostAction action) { + switch (action) { + case UserPostAction.viewProfile: + context.pop(); + navigateToFeedPage(context, feedType: FeedType.user, userId: widget.postViewMedia.postView.creator.id); + break; + case UserPostAction.blockUser: + context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.block, value: true)); + break; + case UserPostAction.unblockUser: + context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.block, value: false)); + break; + case UserPostAction.banUserFromCommunity: + context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.banFromCommunity, value: true)); + break; + case UserPostAction.unbanUserFromCommunity: + context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.banFromCommunity, value: false)); + break; + case UserPostAction.addUserAsCommunityModerator: + context.read().add(UserActionEvent( + userId: widget.postViewMedia.postView.creator.id, + userAction: UserAction.addModerator, + value: true, + metadata: {"communityId": widget.postViewMedia.postView.community.id}, + )); + break; + case UserPostAction.removeUserAsCommunityModerator: + context.read().add(UserActionEvent( + userId: widget.postViewMedia.postView.creator.id, + userAction: UserAction.addModerator, + value: false, + metadata: {"communityId": widget.postViewMedia.postView.community.id}, + )); + break; + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final authState = context.read().state; + + List userActions = UserPostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); + List moderatorActions = UserPostAction.values.where((element) => element.permissionType == PermissionType.moderator).toList(); + List adminActions = UserPostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); + + final account = authState.getSiteResponse?.myUser?.localUserView.person; + final isModerator = authState.getSiteResponse?.myUser?.moderates.where((communityModeratorView) => communityModeratorView.moderator.actorId == account?.actorId).isNotEmpty ?? false; + final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; + + final isLoggedIn = authState.isLoggedIn; + final blockedUsers = authState.getSiteResponse?.myUser?.personBlocks ?? []; + + final isUserBlocked = blockedUsers.where((personBlockView) => personBlockView.person.actorId == widget.postViewMedia.postView.creator.actorId).isNotEmpty; + final isUserCommunityModerator = widget.postViewMedia.postView.creatorIsModerator ?? false; + final isUserBannedFromCommunity = widget.postViewMedia.postView.creatorBannedFromCommunity; + final isUserBannedFromInstance = widget.postViewMedia.postView.creator.banned; + final isUserAdmin = widget.postViewMedia.postView.creatorIsAdmin ?? false; + + if (!isLoggedIn) { + userActions = userActions.where((action) => action.requiresAuthentication == false).toList(); + } else { + if (isUserBlocked) { + userActions = userActions.where((action) => action != UserPostAction.blockUser).toList(); + } else { + userActions = userActions.where((action) => action != UserPostAction.unblockUser).toList(); + } + + if (isUserCommunityModerator) { + moderatorActions = moderatorActions.where((action) => action != UserPostAction.addUserAsCommunityModerator).toList(); + moderatorActions = moderatorActions.where((action) => action != UserPostAction.banUserFromCommunity && action != UserPostAction.unbanUserFromCommunity).toList(); + } else { + moderatorActions = moderatorActions.where((action) => action != UserPostAction.removeUserAsCommunityModerator).toList(); + } + + if (!isUserBannedFromCommunity) { + moderatorActions = moderatorActions.where((action) => action != UserPostAction.banUserFromCommunity).toList(); + } else { + moderatorActions = moderatorActions.where((action) => action != UserPostAction.unbanUserFromCommunity).toList(); + } + + // if (isUserBannedFromInstance) { + // adminActions = adminActions.where((action) => action != UserPostAction.banUser).toList(); + // } else { + // adminActions = adminActions.where((action) => action != UserPostAction.unbanUser).toList(); + // } + + // if (isUserAdmin) { + // adminActions = adminActions.where((action) => action != UserPostAction.addUserAsAdmin).toList(); + // } else { + // adminActions = adminActions.where((action) => action != UserPostAction.removeUserAsAdmin).toList(); + // } + } + + return BlocListener( + listener: (context, state) { + if (state.status == UserStatus.success) { + context.pop(); + widget.onAction(state.personView); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...userActions + .map( + (userPostAction) => BottomSheetAction( + leading: Icon(userPostAction.icon), + title: userPostAction.name, + onTap: () => performAction(userPostAction), + ), + ) + .toList() as List, + if (isModerator && moderatorActions.isNotEmpty) ...[ + const ThunderDivider(sliver: false, padding: false), + ...moderatorActions + .map( + (userPostAction) => BottomSheetAction( + leading: Icon(userPostAction.icon), + trailing: Padding( + padding: const EdgeInsets.only(left: 1), + child: Icon( + Thunder.shield, + size: 20, + color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.green), + ), + ), + title: userPostAction.name, + onTap: () => performAction(userPostAction), + ), + ) + .toList() as List, + ], + if (isAdmin && adminActions.isNotEmpty) ...[ + const ThunderDivider(sliver: false, padding: false), + ...adminActions + .map( + (userPostAction) => BottomSheetAction( + leading: Icon(userPostAction.icon), + trailing: Padding( + padding: const EdgeInsets.only(left: 1), + child: Icon( + Thunder.shield_crown, + size: 20, + color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), + ), + ), + title: userPostAction.name, + onTap: () => performAction(userPostAction), + ), + ) + .toList() as List, + ], + ], + ), + ); + } +} diff --git a/lib/settings/pages/accessibility_settings_page.dart b/lib/settings/pages/accessibility_settings_page.dart index 4e6c1f074..a8bfeaacd 100644 --- a/lib/settings/pages/accessibility_settings_page.dart +++ b/lib/settings/pages/accessibility_settings_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:thunder/community/utils/post_card_action_helpers.dart'; +import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/core/enums/local_settings.dart'; import 'package:thunder/core/singletons/preferences.dart'; @@ -136,7 +136,7 @@ class _AccessibilitySettingsPageState extends State w child: Text( AppLocalizations.of(context)!.accessibilityProfilesDescription, style: TextStyle( - color: theme.colorScheme.onBackground.withOpacity(0.75), + color: theme.colorScheme.onSurface.withOpacity(0.75), ), ), ), diff --git a/lib/shared/bottom_sheet_action.dart b/lib/shared/bottom_sheet_action.dart new file mode 100644 index 000000000..42dbf679e --- /dev/null +++ b/lib/shared/bottom_sheet_action.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class BottomSheetAction extends StatelessWidget { + const BottomSheetAction({super.key, required this.leading, this.trailing, required this.title, this.subtitle, required this.onTap}); + + /// The leading widget + final Widget leading; + + /// The trailing widget + final Widget? trailing; + + /// The title of the category + final String title; + + /// The subtitle of the category + final String? subtitle; + + /// Callback function to be called when the category is tapped + final Function() onTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return InkWell( + onTap: onTap, + customBorder: const StadiumBorder(), + child: ListTile( + leading: leading, + trailing: trailing, + title: Text( + title, + style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + ), + subtitle: subtitle != null + ? Text( + subtitle ?? '', + style: theme.textTheme.bodyMedium?.copyWith(color: theme.textTheme.bodyMedium?.color?.withOpacity(0.8)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : null, + ), + ); + } +} diff --git a/lib/user/bloc/user_bloc.dart b/lib/user/bloc/user_bloc.dart index ca67172a3..be0dcfcd5 100644 --- a/lib/user/bloc/user_bloc.dart +++ b/lib/user/bloc/user_bloc.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -63,6 +64,47 @@ class UserBloc extends Bloc { return emit(state.copyWith(status: UserStatus.failure)); } break; + case UserAction.banFromCommunity: + try { + assert(event.metadata != null); + assert(event.metadata!.containsKey('communityId')); + + int communityId = event.metadata!['communityId'] as int; + String? reason = event.metadata?['reason']; + int? expires = event.metadata?['expires']; + bool removeData = event.metadata?['removeData'] ?? false; + + BanFromCommunityResponse banFromCommunityResponse = await banUserFromCommunity(event.userId, event.value, communityId: communityId, reason: reason, expires: expires, removeData: removeData); + + emit(state.copyWith( + status: UserStatus.success, + personView: banFromCommunityResponse.personView, + message: banFromCommunityResponse.banned + ? l10n.successfullyBannedUser(banFromCommunityResponse.personView.person.name) + : l10n.successfullyUnbannedUser(banFromCommunityResponse.personView.person.name), + )); + } catch (e) { + return emit(state.copyWith(status: UserStatus.failure)); + } + break; + case UserAction.addModerator: + try { + assert(event.metadata != null); + assert(event.metadata!.containsKey('communityId')); + + int communityId = event.metadata!['communityId'] as int; + + AddModToCommunityResponse addModToCommunityResponse = await addModerator(event.userId, event.value, communityId: communityId); + CommunityModeratorView? communityModeratorView = addModToCommunityResponse.moderators.firstWhereOrNull((communityModeratorView) => communityModeratorView.moderator.id == event.userId); + + emit(state.copyWith( + status: UserStatus.success, + message: communityModeratorView != null ? 'Successfully added moderator' : 'Successfully removed moderator', + )); + } catch (e) { + return emit(state.copyWith(status: UserStatus.failure)); + } + break; } } } diff --git a/lib/user/bloc/user_event.dart b/lib/user/bloc/user_event.dart index 27d202ff7..3ca080b98 100644 --- a/lib/user/bloc/user_event.dart +++ b/lib/user/bloc/user_event.dart @@ -18,7 +18,10 @@ final class UserActionEvent extends UserEvent { /// TODO: Change the dynamic type to the correct type(s) if possible final dynamic value; - const UserActionEvent({required this.userId, required this.userAction, this.value}); + /// Additional metadata to attach to the action. This is used for actions such as banning a user + final Map? metadata; + + const UserActionEvent({required this.userId, required this.userAction, this.value, this.metadata}); } final class UserClearMessageEvent extends UserEvent {} diff --git a/lib/user/enums/user_action.dart b/lib/user/enums/user_action.dart index 92969ba15..84e06fa95 100644 --- a/lib/user/enums/user_action.dart +++ b/lib/user/enums/user_action.dart @@ -2,10 +2,11 @@ import 'package:thunder/post/enums/post_action.dart'; enum UserAction { /// User level user actions - block(permissionType: PermissionType.user); + block(permissionType: PermissionType.user), /// Moderator level user actions - // ban(permissionType: PermissionType.moderator), + addModerator(permissionType: PermissionType.moderator), + banFromCommunity(permissionType: PermissionType.moderator); /// Admin level user actions // purge(permissionType: PermissionType.admin); diff --git a/lib/user/utils/user.dart b/lib/user/utils/user.dart index 926bfe13b..66579bbc7 100644 --- a/lib/user/utils/user.dart +++ b/lib/user/utils/user.dart @@ -1,8 +1,10 @@ import 'package:lemmy_api_client/v3.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; +import 'package:thunder/utils/global_context.dart'; /// Logic to block a user Future blockUser(int userId, bool block) async { @@ -19,3 +21,44 @@ Future blockUser(int userId, bool block) async { return blockPersonResponse; } + +/// Logic to ban a user from a community +/// +/// Can optionally provide a reason and expiration date (in seconds) +/// If [removeData] is true, posts and comments from the user will also be deleted +Future banUserFromCommunity(int userId, bool ban, {required int communityId, String? reason, int? expires, bool removeData = false}) async { + final l10n = AppLocalizations.of(GlobalContext.context)!; + final account = await fetchActiveProfileAccount(); + final lemmy = LemmyClient.instance.lemmyApiV3; + + if (account?.jwt == null) throw Exception(l10n.userNotLoggedIn); + + BanFromCommunityResponse banFromCommunityResponse = await lemmy.run(BanFromCommunity( + auth: account!.jwt!, + communityId: communityId, + personId: userId, + ban: ban, + removeData: removeData, + reason: reason, + expires: expires, + )); + + return banFromCommunityResponse; +} + +Future addModerator(int userId, bool added, {required int communityId}) async { + final l10n = AppLocalizations.of(GlobalContext.context)!; + final account = await fetchActiveProfileAccount(); + final lemmy = LemmyClient.instance.lemmyApiV3; + + if (account?.jwt == null) throw Exception(l10n.userNotLoggedIn); + + AddModToCommunityResponse addModToCommunityResponse = await lemmy.run(AddModToCommunity( + auth: account!.jwt!, + communityId: communityId, + personId: userId, + added: added, + )); + + return addModToCommunityResponse; +} From 6b50de843faf57e51a079ae232a9db071f91a4f2 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Mon, 26 Aug 2024 15:14:56 -0700 Subject: [PATCH 2/9] refactor: add post actions, fix some actions not triggering --- lib/feed/bloc/feed_bloc.dart | 10 +- lib/post/utils/post.dart | 18 + .../community_post_action_bottom_sheet.dart | 15 +- .../general_post_action_bottom_sheet.dart | 188 ++++- .../instance_post_action_bottom_sheet.dart | 231 ++++++ .../widgets/post_action_bottom_sheet.dart | 702 ++---------------- .../post_post_action_bottom_sheet.dart | 315 ++++++++ lib/post/widgets/reason_bottom_sheet.dart | 96 --- .../share_post_action_bottom_sheet.dart | 188 +++++ .../user_post_action_bottom_sheet.dart | 17 +- 10 files changed, 1009 insertions(+), 771 deletions(-) create mode 100644 lib/post/widgets/instance_post_action_bottom_sheet.dart create mode 100644 lib/post/widgets/post_post_action_bottom_sheet.dart delete mode 100644 lib/post/widgets/reason_bottom_sheet.dart create mode 100644 lib/post/widgets/share_post_action_bottom_sheet.dart diff --git a/lib/feed/bloc/feed_bloc.dart b/lib/feed/bloc/feed_bloc.dart index f361da409..d17843a69 100644 --- a/lib/feed/bloc/feed_bloc.dart +++ b/lib/feed/bloc/feed_bloc.dart @@ -333,7 +333,15 @@ class FeedBloc extends Bloc { return emit(state.copyWith(status: FeedStatus.failure)); } case PostAction.report: - // TODO: Handle this case. + int existingPostViewMediaIndex = state.postViewMedias.indexWhere((PostViewMedia postViewMedia) => postViewMedia.postView.post.id == event.postId); + PostViewMedia postViewMedia = state.postViewMedias[existingPostViewMediaIndex]; + + try { + await reportPost(postViewMedia.postView.post.id, event.value); + return emit(state.copyWith(status: FeedStatus.success)); + } catch (e) { + return emit(state.copyWith(status: FeedStatus.failure)); + } case PostAction.lock: // Optimistically lock the post int existingPostViewMediaIndex = state.postViewMedias.indexWhere((PostViewMedia postViewMedia) => postViewMedia.postView.post.id == event.postId); diff --git a/lib/post/utils/post.dart b/lib/post/utils/post.dart index 4b80d24f1..51d5d0483 100644 --- a/lib/post/utils/post.dart +++ b/lib/post/utils/post.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; @@ -242,6 +243,23 @@ Future removePost(int postId, bool remove, String reason) async { return postResponse.postView.post.removed == remove; } +/// Logic to remove a post to a community (moderator action) +Future reportPost(int postId, String reason) async { + final l10n = AppLocalizations.of(GlobalContext.context)!; + final account = await fetchActiveProfileAccount(); + final lemmy = LemmyClient.instance.lemmyApiV3; + + if (account?.jwt == null) throw Exception(l10n.userNotLoggedIn); + + PostReportResponse postReportResponse = await lemmy.run(CreatePostReport( + auth: account!.jwt!, + postId: postId, + reason: reason, + )); + + return postReportResponse; +} + /// Logic to vote on a post Future votePost(int postId, int score) async { Account? account = await fetchActiveProfileAccount(); diff --git a/lib/post/widgets/community_post_action_bottom_sheet.dart b/lib/post/widgets/community_post_action_bottom_sheet.dart index 4c3c7a2d6..8b0010ace 100644 --- a/lib/post/widgets/community_post_action_bottom_sheet.dart +++ b/lib/post/widgets/community_post_action_bottom_sheet.dart @@ -10,24 +10,25 @@ import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/models/post_view_media.dart'; import 'package:thunder/feed/feed.dart'; import 'package:thunder/post/enums/post_action.dart'; +import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/shared/bottom_sheet_action.dart'; import 'package:thunder/shared/divider.dart'; import 'package:thunder/thunder/thunder_icons.dart'; /// Defines the actions that can be taken on a community enum CommunityPostAction { - viewCommunity(icon: Icons.person, permissionType: PermissionType.user, requiresAuthentication: false), + viewCommunity(icon: Icons.home_work_rounded, permissionType: PermissionType.user, requiresAuthentication: false), subscribeToCommunity(icon: Icons.add_circle_outline_rounded, permissionType: PermissionType.user, requiresAuthentication: true), unsubscribeFromCommunity(icon: Icons.remove_circle_outline_rounded, permissionType: PermissionType.user, requiresAuthentication: true), - blockCommunity(icon: Icons.block, permissionType: PermissionType.user, requiresAuthentication: true), - unblockCommunity(icon: Icons.block, permissionType: PermissionType.user, requiresAuthentication: true), + blockCommunity(icon: Icons.block_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + unblockCommunity(icon: Icons.block_rounded, permissionType: PermissionType.user, requiresAuthentication: true), ; String get name => switch (this) { - CommunityPostAction.viewCommunity => "View Community", - CommunityPostAction.subscribeToCommunity => "Subscribe To Community", - CommunityPostAction.unsubscribeFromCommunity => "Unsubscribe From Community", - CommunityPostAction.blockCommunity => "Block Community", + CommunityPostAction.viewCommunity => l10n.visitCommunity, + CommunityPostAction.subscribeToCommunity => l10n.subscribeToCommunity, + CommunityPostAction.unsubscribeFromCommunity => l10n.unsubscribeFromCommunity, + CommunityPostAction.blockCommunity => l10n.blockCommunity, CommunityPostAction.unblockCommunity => "Unblock Community", }; diff --git a/lib/post/widgets/general_post_action_bottom_sheet.dart b/lib/post/widgets/general_post_action_bottom_sheet.dart index 464dface6..523b11346 100644 --- a/lib/post/widgets/general_post_action_bottom_sheet.dart +++ b/lib/post/widgets/general_post_action_bottom_sheet.dart @@ -1,20 +1,30 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/enums/full_name.dart'; import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/core/singletons/lemmy_client.dart'; +import 'package:thunder/feed/bloc/feed_bloc.dart'; +import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/shared/bottom_sheet_action.dart'; +import 'package:thunder/shared/multi_picker_item.dart'; +import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/utils/instance.dart'; /// Defines the general actions that can be taken on a post enum GeneralPostAction { general(icon: Icons.more_horiz), - user(icon: Icons.person), - community(icon: Icons.group), - instance(icon: Icons.language), + post(icon: Icons.splitscreen_rounded), + user(icon: Icons.person_rounded), + community(icon: Icons.people_rounded), + instance(icon: Icons.language_rounded), share(icon: Icons.share); String get name => switch (this) { + GeneralPostAction.post => "Post", GeneralPostAction.user => l10n.user, GeneralPostAction.community => l10n.community, GeneralPostAction.instance => l10n.instance(1), @@ -24,6 +34,7 @@ enum GeneralPostAction { /// The title to use for the action. This is shown when the given page is active String get title => switch (this) { + GeneralPostAction.post => "Post Actions", GeneralPostAction.user => l10n.userActions, GeneralPostAction.community => l10n.communityActions, GeneralPostAction.instance => l10n.instanceActions, @@ -37,10 +48,36 @@ enum GeneralPostAction { const GeneralPostAction({required this.icon}); } +enum GeneralQuickPostAction { + upvote(enabledIcon: Icons.arrow_upward_rounded, disabledIcon: Icons.arrow_upward_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + downvote(enabledIcon: Icons.arrow_downward_rounded, disabledIcon: Icons.arrow_downward_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + save(enabledIcon: Icons.star_rounded, disabledIcon: Icons.star_outline_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + read(enabledIcon: Icons.mark_email_read_outlined, disabledIcon: Icons.mark_email_unread_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + hide(enabledIcon: Icons.visibility_off_rounded, disabledIcon: Icons.visibility_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + ; + + /// The icon to use for the action when it is enabled + final IconData enabledIcon; + + /// The icon to use for the action when it is disabled + final IconData disabledIcon; + + /// The permission type to use for the action + final PermissionType permissionType; + + /// Whether or not the action requires user authentication + final bool requiresAuthentication; + + const GeneralQuickPostAction({required this.enabledIcon, required this.disabledIcon, required this.permissionType, required this.requiresAuthentication}); +} + /// Defines the general top-levelactions that can be taken on a post. /// Given a [postViewMedia] and a [onSwitchActivePage] callback, this widget will display a list of actions that can be taken on the post. class GeneralPostActionBottomSheetPage extends StatefulWidget { - const GeneralPostActionBottomSheetPage({super.key, required this.postViewMedia, required this.onSwitchActivePage}); + const GeneralPostActionBottomSheetPage({super.key, required this.context, required this.postViewMedia, required this.onSwitchActivePage}); + + /// The outer context + final BuildContext context; /// The post information final PostViewMedia postViewMedia; @@ -53,38 +90,147 @@ class GeneralPostActionBottomSheetPage extends StatefulWidget { } class _GeneralPostActionBottomSheetPageState extends State { - String generateSubtitle(GeneralPostAction page) { + String? generateSubtitle(GeneralPostAction page) { PostViewMedia postViewMedia = widget.postViewMedia; + String? communityInstance = fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId); + String? userInstance = fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId); + switch (page) { case GeneralPostAction.user: return generateUserFullName(context, postViewMedia.postView.creator.name, postViewMedia.postView.creator.displayName, fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId)); case GeneralPostAction.community: return generateCommunityFullName(context, postViewMedia.postView.community.name, postViewMedia.postView.community.title, fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId)); case GeneralPostAction.instance: - return fetchInstanceNameFromUrl(postViewMedia.postView.post.apId) ?? ''; - case GeneralPostAction.share: - return l10n.share; + return (communityInstance == userInstance) ? '$communityInstance' : '$communityInstance • $userInstance'; default: - return ''; + return null; + } + } + + void performAction(GeneralQuickPostAction action) { + final postViewMedia = widget.postViewMedia; + + switch (action) { + case GeneralQuickPostAction.upvote: + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.vote, postId: postViewMedia.postView.post.id, value: postViewMedia.postView.myVote == 1 ? 0 : 1)); + break; + case GeneralQuickPostAction.downvote: + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.vote, postId: postViewMedia.postView.post.id, value: postViewMedia.postView.myVote == -1 ? 0 : -1)); + break; + case GeneralQuickPostAction.save: + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.save, postId: postViewMedia.postView.post.id, value: !postViewMedia.postView.saved)); + break; + case GeneralQuickPostAction.read: + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.read, postId: postViewMedia.postView.post.id, value: !postViewMedia.postView.read)); + break; + case GeneralQuickPostAction.hide: + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.hide, postId: postViewMedia.postView.post.id, value: postViewMedia.postView.hidden == true ? false : true)); + break; + } + + context.pop(); + } + + IconData getIcon(GeneralQuickPostAction action) { + final postViewMedia = widget.postViewMedia; + + switch (action) { + case GeneralQuickPostAction.upvote: + return postViewMedia.postView.myVote == 1 ? GeneralQuickPostAction.upvote.enabledIcon : GeneralQuickPostAction.upvote.disabledIcon; + case GeneralQuickPostAction.downvote: + return postViewMedia.postView.myVote == -1 ? GeneralQuickPostAction.downvote.enabledIcon : GeneralQuickPostAction.downvote.disabledIcon; + case GeneralQuickPostAction.save: + return postViewMedia.postView.saved ? GeneralQuickPostAction.save.enabledIcon : GeneralQuickPostAction.save.disabledIcon; + case GeneralQuickPostAction.read: + return postViewMedia.postView.read ? GeneralQuickPostAction.read.enabledIcon : GeneralQuickPostAction.read.disabledIcon; + case GeneralQuickPostAction.hide: + return postViewMedia.postView.hidden == true ? GeneralQuickPostAction.hide.enabledIcon : GeneralQuickPostAction.hide.disabledIcon; + } + } + + String getLabel(GeneralQuickPostAction action) { + final postViewMedia = widget.postViewMedia; + + switch (action) { + case GeneralQuickPostAction.upvote: + return postViewMedia.postView.myVote == 1 ? l10n.upvoted : l10n.upvote; + case GeneralQuickPostAction.downvote: + return postViewMedia.postView.myVote == -1 ? l10n.downvoted : l10n.downvote; + case GeneralQuickPostAction.save: + return postViewMedia.postView.saved ? l10n.saved : l10n.save; + case GeneralQuickPostAction.read: + return postViewMedia.postView.read ? "Read" : l10n.markAsRead; + case GeneralQuickPostAction.hide: + return postViewMedia.postView.hidden == true ? l10n.hidden : l10n.hide; + } + } + + Color? getForegroundColor(GeneralQuickPostAction action) { + final state = context.read().state; + final postViewMedia = widget.postViewMedia; + + switch (action) { + case GeneralQuickPostAction.upvote: + return postViewMedia.postView.myVote == 1 ? state.upvoteColor.color : null; + case GeneralQuickPostAction.downvote: + return postViewMedia.postView.myVote == -1 ? state.downvoteColor.color : null; + case GeneralQuickPostAction.save: + return postViewMedia.postView.saved ? state.saveColor.color : null; + case GeneralQuickPostAction.read: + return postViewMedia.postView.read ? state.markReadColor.color : null; + case GeneralQuickPostAction.hide: + return postViewMedia.postView.hidden == true ? state.hideColor.color : null; } } @override Widget build(BuildContext context) { + final authState = context.read().state; + final isLoggedIn = authState.isLoggedIn; + + List userActions = GeneralQuickPostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); + + if (!isLoggedIn) { + userActions = userActions.where((action) => action.requiresAuthentication == false).toList(); + } else { + // Hide hidden if instance does not support it + if (!LemmyClient.instance.supportsFeature(LemmyFeature.hidePosts)) { + userActions = userActions.where((action) => action != GeneralQuickPostAction.hide).toList(); + } + + // Hide downvoted if instance does not support it + if (!authState.downvotesEnabled) { + userActions = userActions.where((action) => action != GeneralQuickPostAction.downvote).toList(); + } + } + return Column( - children: GeneralPostAction.values - .where((page) => page != GeneralPostAction.general) - .map( - (page) => BottomSheetAction( - leading: Icon(page.icon), - trailing: const Icon(Icons.chevron_right_rounded), - title: page.name, - subtitle: generateSubtitle(page), - onTap: () => widget.onSwitchActivePage(page), - ), - ) - .toList() as List, + children: [ + if (userActions.isNotEmpty) + MultiPickerItem( + pickerItems: GeneralQuickPostAction.values + .map((generalQuickPostAction) => PickerItemData( + icon: getIcon(generalQuickPostAction), + label: getLabel(generalQuickPostAction), + foregroundColor: getForegroundColor(generalQuickPostAction), + onSelected: isLoggedIn ? () => performAction(generalQuickPostAction) : null, + )) + .toList(), + ), + ...GeneralPostAction.values + .where((page) => page != GeneralPostAction.general) + .map( + (page) => BottomSheetAction( + leading: Icon(page.icon), + trailing: const Icon(Icons.chevron_right_rounded), + title: page.name, + subtitle: generateSubtitle(page), + onTap: () => widget.onSwitchActivePage(page), + ), + ) + .toList() as List, + ], ); } } diff --git a/lib/post/widgets/instance_post_action_bottom_sheet.dart b/lib/post/widgets/instance_post_action_bottom_sheet.dart new file mode 100644 index 000000000..7ef776cb8 --- /dev/null +++ b/lib/post/widgets/instance_post_action_bottom_sheet.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/instance/bloc/instance_bloc.dart'; +import 'package:thunder/instance/enums/instance_action.dart'; +import 'package:thunder/instance/utils/navigate_instance.dart'; +import 'package:thunder/post/enums/post_action.dart'; +import 'package:thunder/post/utils/comment_action_helpers.dart'; +import 'package:thunder/shared/bottom_sheet_action.dart'; +import 'package:thunder/shared/divider.dart'; +import 'package:thunder/thunder/thunder_icons.dart'; +import 'package:thunder/utils/instance.dart'; + +/// Defines the actions that can be taken on a community +enum InstancePostAction { + visitCommunityInstance(icon: Icons.language_rounded, permissionType: PermissionType.user, requiresAuthentication: false), + blockCommunityInstance(icon: Icons.block_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + unblockCommunityInstance(icon: Icons.block_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + visitUserInstance(icon: Icons.language_rounded, permissionType: PermissionType.user, requiresAuthentication: false), + blockUserInstance(icon: Icons.block_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + unblockUserInstance(icon: Icons.block_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + ; + + String get name => switch (this) { + InstancePostAction.visitCommunityInstance => l10n.visitCommunityInstance, + InstancePostAction.blockCommunityInstance => l10n.blockCommunityInstance, + InstancePostAction.unblockCommunityInstance => "Unblock Community Instance", + InstancePostAction.visitUserInstance => l10n.visitUserInstance, + InstancePostAction.blockUserInstance => l10n.blockUserInstance, + InstancePostAction.unblockUserInstance => "Unblock User Instance", + }; + + /// The icon to use for the action + final IconData icon; + + /// The permission type to use for the action + final PermissionType permissionType; + + /// Whether or not the action requires user authentication + final bool requiresAuthentication; + + const InstancePostAction({required this.icon, required this.permissionType, required this.requiresAuthentication}); +} + +/// A bottom sheet that allows the user to perform actions on a instance. +/// +/// Given a [postViewMedia] and a [onAction] callback, this widget will display a list of actions that can be taken on the instance. +class InstancePostActionBottomSheet extends StatefulWidget { + const InstancePostActionBottomSheet({super.key, required this.postViewMedia, required this.onAction}); + + /// The post information + final PostViewMedia postViewMedia; + + /// Called when an action is selected + final Function() onAction; + + @override + State createState() => _InstancePostActionBottomSheetState(); +} + +class _InstancePostActionBottomSheetState extends State { + void performAction(InstancePostAction action) { + switch (action) { + case InstancePostAction.visitCommunityInstance: + navigateToInstancePage(context, instanceHost: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId)!, instanceId: widget.postViewMedia.postView.community.instanceId); + break; + case InstancePostAction.blockCommunityInstance: + context.read().add(InstanceActionEvent( + instanceAction: InstanceAction.block, + instanceId: widget.postViewMedia.postView.community.instanceId, + domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId), + value: true, + )); + break; + case InstancePostAction.unblockCommunityInstance: + context.read().add(InstanceActionEvent( + instanceAction: InstanceAction.block, + instanceId: widget.postViewMedia.postView.community.instanceId, + domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId), + value: false, + )); + break; + case InstancePostAction.visitUserInstance: + navigateToInstancePage(context, instanceHost: fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId)!, instanceId: widget.postViewMedia.postView.creator.instanceId); + break; + case InstancePostAction.blockUserInstance: + context.read().add(InstanceActionEvent( + instanceAction: InstanceAction.block, + instanceId: widget.postViewMedia.postView.creator.instanceId, + domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId), + value: true, + )); + break; + case InstancePostAction.unblockUserInstance: + context.read().add(InstanceActionEvent( + instanceAction: InstanceAction.block, + instanceId: widget.postViewMedia.postView.creator.instanceId, + domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId), + value: false, + )); + break; + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final authState = context.read().state; + + List userActions = InstancePostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); + List moderatorActions = InstancePostAction.values.where((element) => element.permissionType == PermissionType.moderator).toList(); + List adminActions = InstancePostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); + + final account = authState.getSiteResponse?.myUser?.localUserView.person; + final isModerator = authState.getSiteResponse?.myUser?.moderates.where((communityModeratorView) => communityModeratorView.moderator.actorId == account?.actorId).isNotEmpty ?? false; + final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; + + final isLoggedIn = authState.isLoggedIn; + final blockedInstances = authState.getSiteResponse?.myUser?.instanceBlocks ?? []; + + final communityInstance = fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId); + final userInstance = fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId); + final accountInstance = fetchInstanceNameFromUrl(account?.actorId); + + final isCommunityInstanceBlocked = blockedInstances.where((ibv) => ibv.instance.id == widget.postViewMedia.postView.community.instanceId).isNotEmpty; + final isUserInstanceBlocked = blockedInstances.where((ibv) => ibv.instance.id == widget.postViewMedia.postView.creator.instanceId).isNotEmpty; + + if (!isLoggedIn) { + userActions = userActions.where((action) => action.requiresAuthentication == false).toList(); + } else { + if (isCommunityInstanceBlocked) { + userActions = userActions.where((action) => action != InstancePostAction.blockCommunityInstance).toList(); + } else { + userActions = userActions.where((action) => action != InstancePostAction.unblockCommunityInstance).toList(); + } + + if (isUserInstanceBlocked) { + userActions = userActions.where((action) => action != InstancePostAction.blockUserInstance).toList(); + } else { + userActions = userActions.where((action) => action != InstancePostAction.unblockUserInstance).toList(); + } + } + + if (communityInstance == userInstance) { + userActions.removeWhere((action) => action == InstancePostAction.visitUserInstance || action == InstancePostAction.blockUserInstance); + } + + if (communityInstance == accountInstance) { + userActions.removeWhere((action) => action == InstancePostAction.blockCommunityInstance); + } + + if (userInstance == accountInstance) { + userActions.removeWhere((action) => action == InstancePostAction.blockUserInstance); + } + + return BlocListener( + listener: (context, state) { + if (state.status == InstanceStatus.success) { + context.pop(); + widget.onAction(); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...userActions + .map( + (instancePostAction) => BottomSheetAction( + leading: Icon(instancePostAction.icon), + subtitle: switch (instancePostAction) { + InstancePostAction.visitCommunityInstance => communityInstance, + InstancePostAction.blockCommunityInstance => communityInstance, + InstancePostAction.unblockCommunityInstance => communityInstance, + InstancePostAction.visitUserInstance => userInstance, + InstancePostAction.blockUserInstance => userInstance, + InstancePostAction.unblockUserInstance => userInstance, + }, + title: instancePostAction.name, + onTap: () => performAction(instancePostAction), + ), + ) + .toList() as List, + if (isModerator && moderatorActions.isNotEmpty) ...[ + const ThunderDivider(sliver: false, padding: false), + ...moderatorActions + .map( + (instancePostAction) => BottomSheetAction( + leading: Icon(instancePostAction.icon), + trailing: Padding( + padding: const EdgeInsets.only(left: 1), + child: Icon( + Thunder.shield, + size: 20, + color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.green), + ), + ), + title: instancePostAction.name, + onTap: () => performAction(instancePostAction), + ), + ) + .toList() as List, + ], + if (isAdmin && adminActions.isNotEmpty) ...[ + const ThunderDivider(sliver: false, padding: false), + ...adminActions + .map( + (instancePostAction) => BottomSheetAction( + leading: Icon(instancePostAction.icon), + trailing: Padding( + padding: const EdgeInsets.only(left: 1), + child: Icon( + Thunder.shield_crown, + size: 20, + color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), + ), + ), + title: instancePostAction.name, + onTap: () => performAction(instancePostAction), + ), + ) + .toList() as List, + ], + ], + ), + ); + } +} diff --git a/lib/post/widgets/post_action_bottom_sheet.dart b/lib/post/widgets/post_action_bottom_sheet.dart index e96aef9a5..684682868 100644 --- a/lib/post/widgets/post_action_bottom_sheet.dart +++ b/lib/post/widgets/post_action_bottom_sheet.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -8,44 +11,15 @@ import 'package:thunder/core/models/post_view_media.dart'; import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/post/widgets/community_post_action_bottom_sheet.dart'; import 'package:thunder/post/widgets/general_post_action_bottom_sheet.dart'; +import 'package:thunder/post/widgets/instance_post_action_bottom_sheet.dart'; +import 'package:thunder/post/widgets/post_post_action_bottom_sheet.dart'; +import 'package:thunder/post/widgets/share_post_action_bottom_sheet.dart'; import 'package:thunder/post/widgets/user_post_action_bottom_sheet.dart'; import 'package:thunder/utils/instance.dart'; import 'package:thunder/utils/global_context.dart'; final l10n = AppLocalizations.of(GlobalContext.context)!; -// Defines the actions that can be taken on a post -enum PostPostAction { - upvote, - downvote, - save, - toggleRead, - hide, - share, - delete, - moderatorLockPost, - moderatorPinPost, - moderatorRemovePost, -} - -// Defines the actions that can be taken on an instance -enum InstancePostAction { - visitCommunityInstance, - blockCommunityInstance, - visitUserInstance, - blockUserInstance, -} - -// Defines the actions that can be taken when sharing a post -enum SharePostAction { - sharePost, - sharePostLocal, - shareImage, - shareMedia, - shareLink, - shareAdvanced, -} - /// Programatically show the post action bottom sheet void showPostActionBottomModalSheet( BuildContext context, @@ -57,12 +31,15 @@ void showPostActionBottomModalSheet( context: context, showDragHandle: true, isScrollControlled: true, - builder: (_) => PostActionBottomSheet(postViewMedia: postViewMedia), + builder: (_) => PostActionBottomSheet(context: context, postViewMedia: postViewMedia), ); } class PostActionBottomSheet extends StatefulWidget { - const PostActionBottomSheet({super.key, required this.postViewMedia, this.initialPage = GeneralPostAction.general}); + const PostActionBottomSheet({super.key, required this.context, required this.postViewMedia, this.initialPage = GeneralPostAction.general}); + + /// The parent context + final BuildContext context; /// The post that is being acted on final PostViewMedia postViewMedia; @@ -77,10 +54,44 @@ class PostActionBottomSheet extends StatefulWidget { class _PostActionBottomSheetState extends State { GeneralPostAction currentPage = GeneralPostAction.general; + FutureOr _handleBack(bool stopDefaultButtonEvent, RouteInfo routeInfo) { + if (currentPage != GeneralPostAction.general) { + setState(() => currentPage = GeneralPostAction.general); + return true; + } + + return false; + } + @override void initState() { super.initState(); currentPage = widget.initialPage; + BackButtonInterceptor.add(_handleBack); + } + + @override + void dispose() { + BackButtonInterceptor.remove(_handleBack); + super.dispose(); + } + + String? generateSubtitle(GeneralPostAction page) { + PostViewMedia postViewMedia = widget.postViewMedia; + + String? communityInstance = fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId); + String? userInstance = fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId); + + switch (page) { + case GeneralPostAction.user: + return generateUserFullName(context, postViewMedia.postView.creator.name, postViewMedia.postView.creator.displayName, fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId)); + case GeneralPostAction.community: + return generateCommunityFullName(context, postViewMedia.postView.community.name, postViewMedia.postView.community.title, fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId)); + case GeneralPostAction.instance: + return (communityInstance == userInstance) ? '$communityInstance' : '$communityInstance • $userInstance'; + default: + return null; + } } @override @@ -88,7 +99,13 @@ class _PostActionBottomSheetState extends State { final theme = Theme.of(context); Widget actions = switch (currentPage) { + GeneralPostAction.post => PostPostActionBottomSheet( + context: widget.context, + postViewMedia: widget.postViewMedia, + onAction: () {}, + ), GeneralPostAction.general => GeneralPostActionBottomSheetPage( + context: widget.context, postViewMedia: widget.postViewMedia, onSwitchActivePage: (page) => setState(() => currentPage = page), ), @@ -100,8 +117,15 @@ class _PostActionBottomSheetState extends State { postViewMedia: widget.postViewMedia, onAction: (CommunityView? updatedCommunityView) {}, ), - GeneralPostAction.instance => Container(), - GeneralPostAction.share => Container(), + GeneralPostAction.instance => InstancePostActionBottomSheet( + postViewMedia: widget.postViewMedia, + onAction: () {}, + ), + GeneralPostAction.share => SharePostActionBottomSheet( + context: widget.context, + postViewMedia: widget.postViewMedia, + onAction: () {}, + ), }; return SafeArea( @@ -125,25 +149,7 @@ class _PostActionBottomSheetState extends State { direction: Axis.vertical, children: [ Text(currentPage.title, style: theme.textTheme.titleLarge), - if (currentPage == GeneralPostAction.user) - Text( - generateUserFullName( - context, - widget.postViewMedia.postView.creator.name, - widget.postViewMedia.postView.creator.displayName, - fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId), - ), - ), - if (currentPage == GeneralPostAction.community) - Text( - generateCommunityFullName( - context, - widget.postViewMedia.postView.community.name, - widget.postViewMedia.postView.community.title, - fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId), - ), - ), - if (currentPage == GeneralPostAction.instance) Text(fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId) ?? ''), + if (currentPage != GeneralPostAction.general && currentPage != GeneralPostAction.share && currentPage != GeneralPostAction.post) Text(generateSubtitle(currentPage) ?? ''), ], ), ], @@ -159,227 +165,8 @@ class _PostActionBottomSheetState extends State { } - - - - -// class PostActionBottomSheet { -// const PostActionBottomSheet({ -// required this.postCardAction, -// required this.icon, -// this.trailingIcon, -// required this.label, -// this.getColor, -// this.getForegroundColor, -// this.getOverrideIcon, -// this.getOverrideLabel, -// this.getSubtitleLabel, -// this.shouldShow, -// this.shouldEnable, -// }); - -// final PostCardAction postCardAction; -// final IconData icon; -// final IconData? trailingIcon; -// final String label; -// final Color Function(BuildContext context)? getColor; -// final Color? Function(BuildContext context, PostView postView)? getForegroundColor; -// final IconData? Function(PostView postView)? getOverrideIcon; -// final String? Function(BuildContext context, PostView postView)? getOverrideLabel; -// final String? Function(BuildContext context, PostViewMedia postViewMedia)? getSubtitleLabel; -// final bool Function(BuildContext context, PostView commentView)? shouldShow; -// final bool Function(bool isUserLoggedIn)? shouldEnable; -// } - -// final l10n = AppLocalizations.of(GlobalContext.context)!; - // final List postCardActionItems = [ // PostActionBottomSheet( -// postCardAction: PostCardAction.userActions, -// icon: Icons.person_rounded, -// label: l10n.user, -// getSubtitleLabel: (context, postViewMedia) => generateUserFullName( -// context, -// postViewMedia.postView.creator.name, -// postViewMedia.postView.creator.displayName, -// fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId), -// ), -// trailingIcon: Icons.chevron_right_rounded, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.visitProfile, -// icon: Icons.person_search_rounded, -// label: l10n.visitUserProfile, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.blockUser, -// icon: Icons.block, -// label: l10n.blockUser, -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.communityActions, -// icon: Icons.people_rounded, -// label: l10n.community, -// getSubtitleLabel: (context, postViewMedia) => generateCommunityFullName( -// context, -// postViewMedia.postView.community.name, -// postViewMedia.postView.community.title, -// fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), -// ), -// trailingIcon: Icons.chevron_right_rounded, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.visitCommunity, -// icon: Icons.home_work_rounded, -// label: l10n.visitCommunity, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.subscribeToCommunity, -// icon: Icons.add_circle_outline_rounded, -// label: l10n.subscribeToCommunity, -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.unsubscribeFromCommunity, -// icon: Icons.remove_circle_outline_rounded, -// label: l10n.unsubscribeFromCommunity, -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.blockCommunity, -// icon: Icons.block_rounded, -// label: l10n.blockCommunity, -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.instanceActions, -// icon: Icons.language_rounded, -// label: l10n.instance(1), -// getSubtitleLabel: (context, postViewMedia) { -// return areCommunityAndUserOnSameInstance(postViewMedia.postView) -// ? fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId) -// : '${fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId)} • ${fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId)}'; -// }, -// trailingIcon: Icons.chevron_right_rounded, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.visitCommunityInstance, -// icon: Icons.language, -// label: '', -// getOverrideLabel: (context, postView) { -// return areCommunityAndUserOnSameInstance(postView) ? l10n.visitInstance : l10n.visitCommunityInstance; -// }, -// getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.blockCommunityInstance, -// icon: Icons.block_rounded, -// label: '', -// getOverrideLabel: (context, postView) { -// return areCommunityAndUserOnSameInstance(postView) ? l10n.blockInstance : l10n.blockCommunityInstance; -// }, -// getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.visitUserInstance, -// icon: Icons.language, -// label: l10n.visitUserInstance, -// getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId), -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.blockUserInstance, -// icon: Icons.block_rounded, -// label: l10n.blockUserInstance, -// getSubtitleLabel: (context, postViewMedia) => fetchInstanceNameFromUrl(postViewMedia.postView.creator.actorId), -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.sharePost, -// icon: Icons.share_rounded, -// label: l10n.sharePost, -// getSubtitleLabel: (context, postViewMedia) => postViewMedia.postView.post.apId, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.sharePostLocal, -// icon: Icons.share_rounded, -// label: l10n.sharePostLocal, -// getSubtitleLabel: (context, postViewMedia) => LemmyClient.instance.generatePostUrl(postViewMedia.postView.post.id), -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.shareImage, -// icon: Icons.image_rounded, -// label: l10n.shareImage, -// getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.imageUrl, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.shareMedia, -// icon: Icons.personal_video_rounded, -// label: l10n.shareMediaLink, -// getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.mediaUrl, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.shareLink, -// icon: Icons.link_rounded, -// label: l10n.shareLink, -// getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.originalUrl, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.shareAdvanced, -// icon: Icons.screen_share_rounded, -// label: l10n.advanced, -// getSubtitleLabel: (context, postViewMedia) => l10n.useAdvancedShareSheet, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.upvote, -// label: l10n.upvote, -// icon: Icons.arrow_upward_rounded, -// getColor: (context) => context.read().state.upvoteColor.color, -// getForegroundColor: (context, postView) => postView.myVote == 1 ? context.read().state.upvoteColor.color : null, -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.downvote, -// label: l10n.downvote, -// icon: Icons.arrow_downward_rounded, -// getColor: (context) => context.read().state.downvoteColor.color, -// getForegroundColor: (context, postView) => postView.myVote == -1 ? context.read().state.downvoteColor.color : null, -// shouldShow: (context, commentView) => context.read().state.downvotesEnabled, -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.save, -// label: l10n.save, -// icon: Icons.star_border_rounded, -// getColor: (context) => context.read().state.saveColor.color, -// getForegroundColor: (context, postView) => postView.saved ? context.read().state.saveColor.color : null, -// getOverrideIcon: (postView) => postView.saved ? Icons.star_rounded : null, -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.toggleRead, -// label: l10n.toggelRead, -// icon: Icons.mail_outline_outlined, -// getColor: (context) => context.read().state.markReadColor.color, -// getOverrideIcon: (postView) => postView.read ? Icons.mark_email_unread_rounded : Icons.mark_email_read_outlined, -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.hide, -// label: l10n.hide, -// getOverrideLabel: (context, postView) => postView.hidden == true ? l10n.unhide : l10n.hide, -// icon: Icons.visibility_off_rounded, -// getColor: (context) => context.read().state.hideColor.color, -// getOverrideIcon: (postView) => postView.hidden == true ? Icons.visibility_rounded : Icons.visibility_off_rounded, -// shouldEnable: (isUserLoggedIn) => isUserLoggedIn, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.share, -// icon: Icons.share_rounded, -// label: l10n.share, -// ), -// PostActionBottomSheet( // postCardAction: PostCardAction.delete, // icon: Icons.delete_rounded, // label: l10n.delete, @@ -387,12 +174,6 @@ class _PostActionBottomSheetState extends State { // getOverrideLabel: (context, postView) => postView.post.deleted ? l10n.restore : l10n.delete, // ), // PostActionBottomSheet( -// postCardAction: PostCardAction.moderatorActions, -// icon: Icons.shield_rounded, -// trailingIcon: Icons.chevron_right_rounded, -// label: l10n.moderatorActions, -// ), -// PostActionBottomSheet( // postCardAction: PostCardAction.moderatorLockPost, // icon: Icons.lock, // label: l10n.lockPost, @@ -415,14 +196,6 @@ class _PostActionBottomSheetState extends State { // ) // ]; -// enum PostActionBottomSheetPage { -// general, -// share, -// moderator, -// user, -// community, -// instance, -// } // void showPostActionBottomModalSheet( // BuildContext context, @@ -432,42 +205,12 @@ class _PostActionBottomSheetState extends State { // void Function(int userId)? onBlockedCommunity, // void Function(int postId)? onPostHidden, // }) { -// final bool isOwnPost = postViewMedia.postView.creator.id == context.read().state.account?.userId; -// final bool isModerator = -// context.read().state.moderates.any((CommunityModeratorView communityModeratorView) => communityModeratorView.community.id == postViewMedia.postView.community.id); -// final int? currentUserId = context.read().state.account?.userId; - -// // Generate the list of default actions for the general page -// final List defaultPostCardActions = postCardActionItems -// .where((extendedAction) => [ -// PostCardAction.userActions, -// PostCardAction.communityActions, -// PostCardAction.instanceActions, -// ].contains(extendedAction.postCardAction)) -// .toList(); // // Add the moderator actions submenu // if (isModerator) { // defaultPostCardActions.add(postCardActionItems.firstWhere((PostActionBottomSheet extendedPostCardActions) => extendedPostCardActions.postCardAction == PostCardAction.moderatorActions)); // } -// // Generate the list of default multi actions -// final List defaultMultiPostCardActions = postCardActionItems -// .where((extendedAction) => [ -// PostCardAction.upvote, -// PostCardAction.downvote, -// PostCardAction.save, -// PostCardAction.toggleRead, -// PostCardAction.hide, -// PostCardAction.share, -// if (isOwnPost) PostCardAction.delete, -// ].contains(extendedAction.postCardAction)) -// .toList(); - -// // Remove hide if unsupported -// if (defaultMultiPostCardActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.hide) && !LemmyClient.instance.supportsFeature(LemmyFeature.hidePosts)) { -// defaultMultiPostCardActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.hide); -// } // // Generate the list of moderator actions // final List moderatorPostCardActions = postCardActionItems @@ -478,49 +221,6 @@ class _PostActionBottomSheetState extends State { // ].contains(extendedAction.postCardAction)) // .toList(); -// // Generate the list of share actions -// final List sharePostCardActions = postCardActionItems -// .where((extendedAction) => [ -// PostCardAction.sharePost, -// PostCardAction.sharePostLocal, -// PostCardAction.shareImage, -// PostCardAction.shareMedia, -// PostCardAction.shareLink, -// PostCardAction.shareAdvanced, -// ].contains(extendedAction.postCardAction)) -// .toList(); - -// // Remove the share link option if there is no link -// // Or if the media link is the same as the external link -// if (postViewMedia.media.isEmpty || -// postViewMedia.media.first.mediaType == MediaType.text || -// postViewMedia.media.first.originalUrl == postViewMedia.media.first.imageUrl || -// postViewMedia.media.first.originalUrl == postViewMedia.media.first.mediaUrl) { -// sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareLink); -// } - -// // Remove the share image option if there is no image -// if (postViewMedia.media.isEmpty || postViewMedia.media.first.imageUrl?.isNotEmpty != true) { -// sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareImage); -// } - -// // Remove the share media option if there is no media -// if (postViewMedia.media.isEmpty || postViewMedia.media.first.mediaUrl?.isNotEmpty != true) { -// sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareMedia); -// } - -// // Remove the share local option if it is the same as the original -// if (postViewMedia.postView.post.apId == LemmyClient.instance.generatePostUrl(postViewMedia.postView.post.id)) { -// sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.sharePostLocal); -// } - -// // Generate the list of user actions -// final List userActions = postCardActionItems -// .where((extendedAction) => [ -// PostCardAction.visitProfile, -// if (postViewMedia.postView.creator.id != currentUserId) PostCardAction.blockUser, -// ].contains(extendedAction.postCardAction)) -// .toList(); // // Generate the list of community actions // final List communityActions = postCardActionItems @@ -595,68 +295,10 @@ class _PostActionBottomSheetState extends State { // ); // } -// class PostCardActionPicker extends StatefulWidget { -// /// The post -// final PostViewMedia postViewMedia; - -// /// This is the list of quick actions that are shown horizontally across the top of the sheet -// final Map> multiPostCardActions; - -// /// This is the set of full actions to display vertically in a list -// final Map> postCardActions; - -// /// This is the set of titles to show for each page -// final Map titles; - -// /// The current page -// final PostActionBottomSheetPage page; - -// /// The context from whoever invoked this sheet (useful for blocs that would otherwise be missing) -// final BuildContext outerContext; - -// /// Callback used to notify that we blocked a user -// final void Function(int userId)? onBlockedUser; - -// /// Callback used to notify that we blocked a community -// final Function(int userId)? onBlockedCommunity; - -// /// Callback used to notify that we hid a post -// final Function(int postId)? onPostHidden; - -// const PostCardActionPicker({ -// super.key, -// required this.postViewMedia, -// required this.page, -// required this.postCardActions, -// required this.multiPostCardActions, -// required this.titles, -// required this.outerContext, -// required this.onBlockedUser, -// required this.onBlockedCommunity, -// required this.onPostHidden, -// }); - -// @override -// State createState() => _PostCardActionPickerState(); -// } // class _PostCardActionPickerState extends State { // PostActionBottomSheetPage? page; -// @override -// void initState() { -// super.initState(); - -// BackButtonInterceptor.add(_handleBack); -// } - -// @override -// void dispose() { -// BackButtonInterceptor.remove(_handleBack); - -// super.dispose(); -// } - // @override // Widget build(BuildContext context) { // final ThemeData theme = Theme.of(context); @@ -671,41 +313,6 @@ class _PostActionBottomSheetState extends State { // mainAxisAlignment: MainAxisAlignment.start, // mainAxisSize: MainAxisSize.max, // children: [ -// Semantics( -// label: '${widget.titles[page ?? widget.page] ?? l10n.actions}, ${(page ?? widget.page) == PostActionBottomSheetPage.general ? '' : l10n.backButton}', -// child: Padding( -// padding: const EdgeInsets.only(left: 10, right: 10), -// child: Material( -// borderRadius: BorderRadius.circular(50), -// color: Colors.transparent, -// child: InkWell( -// borderRadius: BorderRadius.circular(50), -// onTap: (page ?? widget.page) == PostActionBottomSheetPage.general ? null : () => setState(() => page = PostActionBottomSheetPage.general), -// child: Padding( -// padding: const EdgeInsets.fromLTRB(12.0, 10, 16.0, 10.0), -// child: Align( -// alignment: Alignment.centerLeft, -// child: Row( -// children: [ -// if ((page ?? widget.page) != PostActionBottomSheetPage.general) ...[ -// const Icon(Icons.chevron_left, size: 30), -// const SizedBox(width: 12), -// ], -// Semantics( -// excludeSemantics: true, -// child: Text( -// widget.titles[page ?? widget.page] ?? l10n.actions, -// style: theme.textTheme.titleLarge, -// ), -// ), -// ], -// ), -// ), -// ), -// ), -// ), -// ), -// ), // // Post metadata chips // if ((page ?? PostActionBottomSheetPage.general) == PostActionBottomSheetPage.general) // Row( @@ -730,25 +337,7 @@ class _PostActionBottomSheetState extends State { // ), // ], // ), -// if (widget.postCardActions[page ?? widget.page]?.isNotEmpty == true) -// ListView.builder( -// shrinkWrap: true, -// physics: const NeverScrollableScrollPhysics(), -// itemCount: widget.postCardActions[page ?? widget.page]!.length, -// itemBuilder: (BuildContext itemBuilderContext, int index) { -// return PickerItem( -// label: widget.postCardActions[page ?? widget.page]![index].getOverrideLabel?.call(context, widget.postViewMedia.postView) ?? -// widget.postCardActions[page ?? widget.page]![index].label, -// subtitle: widget.postCardActions[page ?? widget.page]![index].getSubtitleLabel?.call(context, widget.postViewMedia), -// icon: widget.postCardActions[page ?? widget.page]![index].getOverrideIcon?.call(widget.postViewMedia.postView) ?? widget.postCardActions[page ?? widget.page]![index].icon, -// trailingIcon: widget.postCardActions[page ?? widget.page]![index].trailingIcon, -// onSelected: (widget.postCardActions[page ?? widget.page]![index].shouldEnable?.call(isUserLoggedIn) ?? true) -// ? () => onSelected(widget.postCardActions[page ?? widget.page]![index].postCardAction) -// : null, -// ); -// }, -// ), -// const SizedBox(height: 16.0), + // ], // ), // ), @@ -757,100 +346,7 @@ class _PostActionBottomSheetState extends State { // } // void onSelected(PostCardAction postCardAction) async { -// bool pop = true; -// void Function() action; - // switch (postCardAction) { -// case PostCardAction.visitCommunity: -// action = () => onTapCommunityName(widget.outerContext, widget.postViewMedia.postView.community.id); -// break; -// case PostCardAction.userActions: -// action = () => setState(() => page = PostActionBottomSheetPage.user); -// pop = false; -// break; -// case PostCardAction.visitProfile: -// action = () => navigateToFeedPage(widget.outerContext, feedType: FeedType.user, userId: widget.postViewMedia.postView.post.creatorId); -// break; -// case PostCardAction.visitCommunityInstance: -// action = () => navigateToInstancePage(widget.outerContext, -// instanceHost: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId)!, instanceId: widget.postViewMedia.postView.community.instanceId); -// break; -// case PostCardAction.visitUserInstance: -// action = () => navigateToInstancePage(widget.outerContext, -// instanceHost: fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId)!, instanceId: widget.postViewMedia.postView.creator.instanceId); -// break; -// case PostCardAction.sharePost: -// action = () => Share.share(widget.postViewMedia.postView.post.apId); -// break; -// case PostCardAction.sharePostLocal: -// action = () => Share.share(LemmyClient.instance.generatePostUrl(widget.postViewMedia.postView.post.id)); -// break; -// case PostCardAction.shareImage: -// action = () async { -// if (widget.postViewMedia.media.first.imageUrl != null) { -// try { -// // Try to get the cached image first -// var media = await DefaultCacheManager().getFileFromCache(widget.postViewMedia.media.first.imageUrl!); -// File? mediaFile = media?.file; - -// if (media == null) { -// // Tell user we're downloading the image -// showSnackbar(AppLocalizations.of(widget.outerContext)!.downloadingMedia); - -// // Download -// mediaFile = await DefaultCacheManager().getSingleFile(widget.postViewMedia.media.first.imageUrl!); -// } - -// // Share -// await Share.shareXFiles([XFile(mediaFile!.path)]); -// } catch (e) { -// // Tell the user that the download failed -// showSnackbar(AppLocalizations.of(widget.outerContext)!.errorDownloadingMedia(e)); -// } -// } -// }; -// break; -// case PostCardAction.shareMedia: -// action = () => Share.share(widget.postViewMedia.media.first.mediaUrl!); -// break; -// case PostCardAction.shareLink: -// action = () { -// if (widget.postViewMedia.media.first.originalUrl != null) Share.share(widget.postViewMedia.media.first.originalUrl!); -// }; -// break; -// case PostCardAction.shareAdvanced: -// action = () => showAdvancedShareSheet(widget.outerContext, widget.postViewMedia); -// break; -// case PostCardAction.instanceActions: -// action = () => setState(() => page = PostActionBottomSheetPage.instance); -// pop = false; -// break; -// case PostCardAction.blockCommunityInstance: -// action = () => widget.outerContext.read().add(InstanceActionEvent( -// instanceAction: InstanceAction.block, -// instanceId: widget.postViewMedia.postView.community.instanceId, -// domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId), -// value: true, -// )); -// break; -// case PostCardAction.blockUserInstance: -// action = () => widget.outerContext.read().add(InstanceActionEvent( -// instanceAction: InstanceAction.block, -// instanceId: widget.postViewMedia.postView.creator.instanceId, -// domain: fetchInstanceNameFromUrl(widget.postViewMedia.postView.creator.actorId), -// value: true, -// )); -// break; -// case PostCardAction.communityActions: -// action = () => setState(() => page = PostActionBottomSheetPage.community); -// pop = false; -// break; -// case PostCardAction.blockCommunity: -// action = () { -// widget.outerContext.read().add(CommunityActionEvent(communityAction: CommunityAction.block, communityId: widget.postViewMedia.postView.community.id, value: true)); -// widget.onBlockedCommunity?.call(widget.postViewMedia.postView.community.id); -// }; -// break; // case PostCardAction.upvote: // action = () => widget.outerContext // .read() @@ -875,30 +371,6 @@ class _PostActionBottomSheetState extends State { // .add(FeedItemActionedEvent(postAction: PostAction.hide, postId: widget.postViewMedia.postView.post.id, value: !(widget.postViewMedia.postView.hidden ?? false))); // widget.onPostHidden?.call(widget.postViewMedia.postView.post.id); // break; -// case PostCardAction.share: -// pop = false; -// action = () => setState(() => page = PostActionBottomSheetPage.share); -// break; -// case PostCardAction.blockUser: -// action = () { -// widget.outerContext.read().add(UserActionEvent(userAction: UserAction.block, userId: widget.postViewMedia.postView.creator.id, value: true)); -// widget.onBlockedCommunity?.call(widget.postViewMedia.postView.creator.id); -// }; -// break; -// case PostCardAction.subscribeToCommunity: -// action = () => widget.outerContext.read().add(CommunityActionEvent( -// communityAction: CommunityAction.follow, -// communityId: widget.postViewMedia.postView.community.id, -// value: true, -// )); -// break; -// case PostCardAction.unsubscribeFromCommunity: -// action = () => widget.outerContext.read().add(CommunityActionEvent( -// communityAction: CommunityAction.follow, -// communityId: widget.postViewMedia.postView.community.id, -// value: false, -// )); -// break; // case PostCardAction.delete: // action = () => widget.outerContext // .read() @@ -922,56 +394,6 @@ class _PostActionBottomSheetState extends State { // action = () => showRemovePostReasonBottomSheet(widget.outerContext, widget.postViewMedia); // break; // } - -// if (pop) { -// Navigator.of(context).pop(); -// } - -// action(); -// } - -// FutureOr _handleBack(bool stopDefaultButtonEvent, RouteInfo routeInfo) { -// if ((page ?? widget.page) != PostActionBottomSheetPage.general) { -// setState(() => page = PostActionBottomSheetPage.general); -// return true; -// } - -// return false; // } // } -// void onTapCommunityName(BuildContext context, int communityId) { -// navigateToFeedPage(context, feedType: FeedType.community, communityId: communityId); -// } - -// void showRemovePostReasonBottomSheet(BuildContext context, PostViewMedia postViewMedia) { -// showModalBottomSheet( -// context: context, -// showDragHandle: true, -// isScrollControlled: true, -// builder: (_) => ReasonBottomSheet( -// title: postViewMedia.postView.post.removed ? l10n.restorePost : l10n.removalReason, -// submitLabel: postViewMedia.postView.post.removed ? l10n.restore : l10n.remove, -// textHint: l10n.reason, -// onSubmit: (String message) { -// context.read().add( -// FeedItemActionedEvent( -// postAction: PostAction.remove, -// postId: postViewMedia.postView.post.id, -// value: { -// 'remove': !postViewMedia.postView.post.removed, -// 'reason': message, -// }, -// ), -// ); -// Navigator.of(context).pop(); -// }, -// ), -// ); -// } - -// bool areCommunityAndUserOnSameInstance(PostView postView) { -// String? communityInstance = fetchInstanceNameFromUrl(postView.community.actorId); -// String? userInstance = fetchInstanceNameFromUrl(postView.creator.actorId); -// return communityInstance == userInstance; -// } diff --git a/lib/post/widgets/post_post_action_bottom_sheet.dart b/lib/post/widgets/post_post_action_bottom_sheet.dart new file mode 100644 index 000000000..87ebb9609 --- /dev/null +++ b/lib/post/widgets/post_post_action_bottom_sheet.dart @@ -0,0 +1,315 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/feed/bloc/feed_bloc.dart'; +import 'package:thunder/post/enums/post_action.dart'; +import 'package:thunder/post/utils/comment_action_helpers.dart'; +import 'package:thunder/shared/bottom_sheet_action.dart'; +import 'package:thunder/shared/dialogs.dart'; +import 'package:thunder/shared/divider.dart'; +import 'package:thunder/thunder/thunder_icons.dart'; + +/// Defines the actions that can be taken on a user +/// TODO: Implement admin-level actions +enum PostPostAction { + reportPost(icon: Icons.flag_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + editPost(icon: Icons.edit_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + deletePost(icon: Icons.delete_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + restorePost(icon: Icons.restore_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + lockPost(icon: Icons.lock_rounded, permissionType: PermissionType.moderator, requiresAuthentication: true), + unlockPost(icon: Icons.lock_open_rounded, permissionType: PermissionType.moderator, requiresAuthentication: true), + removePost(icon: Icons.delete_rounded, permissionType: PermissionType.moderator, requiresAuthentication: true), + restorePostAsModerator(icon: Icons.restore_rounded, permissionType: PermissionType.moderator, requiresAuthentication: true), + pinPostToCommunity(icon: Icons.pin, permissionType: PermissionType.moderator, requiresAuthentication: true), + unpinPostFromCommunity(icon: Icons.pin, permissionType: PermissionType.moderator, requiresAuthentication: true), + // pinPostToInstance(icon: Icons.pin, permissionType: PermissionType.admin, requiresAuthentication: true), + // unpinPostFromInstance(icon: Icons.pin, permissionType: PermissionType.admin, requiresAuthentication: true), + ; + + String get name => switch (this) { + PostPostAction.reportPost => "Report Post", + PostPostAction.editPost => l10n.editPost, + PostPostAction.deletePost => "Delete Post", + PostPostAction.restorePost => l10n.restorePost, + PostPostAction.lockPost => l10n.lockPost, + PostPostAction.unlockPost => l10n.unlockPost, + PostPostAction.removePost => l10n.removePost, + PostPostAction.restorePostAsModerator => "Restore Post", + PostPostAction.pinPostToCommunity => "Pin Post To Community", + PostPostAction.unpinPostFromCommunity => "Unpin Post From Community", + // PostPostAction.pinPostToInstance => "Pin Post To Instance", + // PostPostAction.unpinPostFromInstance => "Unpin Post From Instance", + }; + + /// The icon to use for the action + final IconData icon; + + /// The permission type to use for the action + final PermissionType permissionType; + + /// Whether or not the action requires user authentication + final bool requiresAuthentication; + + const PostPostAction({required this.icon, required this.permissionType, required this.requiresAuthentication}); +} + +/// A bottom sheet that allows the user to perform actions on the post. +/// +/// Given a [postViewMedia] and a [onAction] callback, this widget will display a list of actions that can be taken on the post. +/// The [onAction] callback will be triggered when an action is performed. +class PostPostActionBottomSheet extends StatefulWidget { + const PostPostActionBottomSheet({super.key, required this.context, required this.postViewMedia, required this.onAction}); + + /// The outer context + final BuildContext context; + + /// The post information + final PostViewMedia postViewMedia; + + /// Called when an action is selected + final Function() onAction; + + @override + State createState() => _PostPostActionBottomSheetState(); +} + +class _PostPostActionBottomSheetState extends State { + void performAction(PostPostAction action) { + final postViewMedia = widget.postViewMedia; + + switch (action) { + case PostPostAction.reportPost: + showReportPostDialog(); + return; + case PostPostAction.editPost: + context.pop(); + return; + case PostPostAction.deletePost: + context.pop(); + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.delete, postId: postViewMedia.postView.post.id, value: true)); + break; + case PostPostAction.restorePost: + context.pop(); + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.delete, postId: postViewMedia.postView.post.id, value: false)); + break; + case PostPostAction.lockPost: + context.pop(); + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.lock, postId: postViewMedia.postView.post.id, value: true)); + break; + case PostPostAction.unlockPost: + context.pop(); + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.lock, postId: postViewMedia.postView.post.id, value: false)); + break; + case PostPostAction.removePost: + showRemovePostReasonDialog(); + break; + case PostPostAction.restorePostAsModerator: + showRemovePostReasonDialog(); + break; + case PostPostAction.pinPostToCommunity: + context.pop(); + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.pinCommunity, postId: postViewMedia.postView.post.id, value: true)); + break; + case PostPostAction.unpinPostFromCommunity: + context.pop(); + widget.context.read().add(FeedItemActionedEvent(postAction: PostAction.pinCommunity, postId: postViewMedia.postView.post.id, value: false)); + break; + // case PostPostAction.pinPostToInstance: + // context.pop(); + // return; + // case PostPostAction.unpinPostFromInstance: + // context.pop(); + // return; + } + } + + void showReportPostDialog() { + context.pop(); + final TextEditingController messageController = TextEditingController(); + + showThunderDialog( + context: widget.context, + title: "Report Post", + primaryButtonText: "Report", + onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { + widget.context.read().add( + FeedItemActionedEvent( + postAction: PostAction.report, + postId: widget.postViewMedia.postView.post.id, + value: messageController.text, + ), + ); + dialogContext.pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (context) => context.pop(), + contentWidgetBuilder: (_) => TextFormField( + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: l10n.message(0), + ), + autofocus: true, + controller: messageController, + maxLines: 4, + ), + ); + } + + void showRemovePostReasonDialog() { + context.pop(); + final TextEditingController messageController = TextEditingController(); + + showThunderDialog( + context: widget.context, + title: widget.postViewMedia.postView.post.removed ? l10n.restorePost : l10n.removalReason, + primaryButtonText: widget.postViewMedia.postView.post.removed ? l10n.restore : l10n.remove, + onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { + widget.context.read().add( + FeedItemActionedEvent( + postAction: PostAction.remove, + postId: widget.postViewMedia.postView.post.id, + value: { + 'remove': !widget.postViewMedia.postView.post.removed, + 'reason': messageController.text, + }, + ), + ); + dialogContext.pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (context) => context.pop(), + contentWidgetBuilder: (_) => TextFormField( + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: l10n.message(0), + ), + autofocus: true, + controller: messageController, + maxLines: 4, + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final authState = context.read().state; + + List userActions = PostPostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); + List moderatorActions = PostPostAction.values.where((element) => element.permissionType == PermissionType.moderator).toList(); + List adminActions = PostPostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); + + final account = authState.getSiteResponse?.myUser?.localUserView.person; + final isModerator = + authState.getSiteResponse?.myUser?.moderates.where((communityModeratorView) => communityModeratorView.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty ?? false; + final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; + + final isLoggedIn = authState.isLoggedIn; + final isPostLocked = widget.postViewMedia.postView.post.locked; + final isPostPinnedToCommunity = widget.postViewMedia.postView.post.featuredCommunity; // Pin to community + final isPostPinnedToInstance = widget.postViewMedia.postView.post.featuredLocal; // Pin to instance + final isPostDeleted = widget.postViewMedia.postView.post.deleted; // Deleted by the user + final isPostRemoved = widget.postViewMedia.postView.post.removed; // Removed by a moderator + + if (!isLoggedIn) { + userActions = userActions.where((action) => action.requiresAuthentication == false).toList(); + } else { + if (account?.actorId == widget.postViewMedia.postView.creator.actorId) { + userActions = userActions.where((action) => action != PostPostAction.reportPost).toList(); + } else { + userActions = userActions.where((action) => action != PostPostAction.editPost && action != PostPostAction.deletePost && action != PostPostAction.restorePost).toList(); + } + + if (isPostDeleted) { + userActions = userActions.where((action) => action != PostPostAction.deletePost).toList(); + } else { + userActions = userActions.where((action) => action != PostPostAction.restorePost).toList(); + } + + if (isPostRemoved) { + moderatorActions = moderatorActions.where((action) => action != PostPostAction.removePost).toList(); + } else { + moderatorActions = moderatorActions.where((action) => action != PostPostAction.restorePostAsModerator).toList(); + } + + if (isPostLocked) { + moderatorActions = moderatorActions.where((action) => action != PostPostAction.lockPost).toList(); + } else { + moderatorActions = moderatorActions.where((action) => action != PostPostAction.unlockPost).toList(); + } + + if (isPostPinnedToCommunity) { + moderatorActions = moderatorActions.where((action) => action != PostPostAction.pinPostToCommunity).toList(); + } else { + moderatorActions = moderatorActions.where((action) => action != PostPostAction.unpinPostFromCommunity).toList(); + } + + // if (isPostPinnedToInstance) { + // adminActions = adminActions.where((action) => action != PostPostAction.pinPostToInstance).toList(); + // } else { + // adminActions = adminActions.where((action) => action != PostPostAction.unpinPostFromInstance).toList(); + // } + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...userActions + .map( + (postPostAction) => BottomSheetAction( + leading: Icon(postPostAction.icon), + title: postPostAction.name, + onTap: () => performAction(postPostAction), + ), + ) + .toList() as List, + if (isModerator && moderatorActions.isNotEmpty) ...[ + const ThunderDivider(sliver: false, padding: false), + ...moderatorActions + .map( + (postPostAction) => BottomSheetAction( + leading: Icon(postPostAction.icon), + trailing: Padding( + padding: const EdgeInsets.only(left: 1), + child: Icon( + Thunder.shield, + size: 20, + color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.green), + ), + ), + title: postPostAction.name, + onTap: () => performAction(postPostAction), + ), + ) + .toList() as List, + ], + if (isAdmin && adminActions.isNotEmpty) ...[ + const ThunderDivider(sliver: false, padding: false), + ...adminActions + .map( + (postPostAction) => BottomSheetAction( + leading: Icon(postPostAction.icon), + trailing: Padding( + padding: const EdgeInsets.only(left: 1), + child: Icon( + Thunder.shield_crown, + size: 20, + color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), + ), + ), + title: postPostAction.name, + onTap: () => performAction(postPostAction), + ), + ) + .toList() as List, + ], + ], + ); + } +} diff --git a/lib/post/widgets/reason_bottom_sheet.dart b/lib/post/widgets/reason_bottom_sheet.dart deleted file mode 100644 index ec43384dd..000000000 --- a/lib/post/widgets/reason_bottom_sheet.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ReasonBottomSheet extends StatefulWidget { - const ReasonBottomSheet({super.key, this.title, this.textHint, this.submitLabel, this.errorMessage, required this.onSubmit}); - - /// A custom title of the bottom sheet. Defaults to "Reason" - final String? title; - - /// A custom text hint of the text field. Defaults to "Message" - final String? textHint; - - /// A custom label of the submit button. Defaults to "Submit" - final String? submitLabel; - - /// An error message to display - final String? errorMessage; - - /// Callback function which triggers when the submit button is pressed - final Function(String) onSubmit; - - @override - State createState() => _ReasonBottomSheetState(); -} - -class _ReasonBottomSheetState extends State { - late TextEditingController messageController; - - @override - void initState() { - messageController = TextEditingController(); - super.initState(); - } - - @override - void dispose() { - messageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final l10n = AppLocalizations.of(context)!; - - return Container( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom, left: 26.0, right: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - widget.title ?? l10n.reason, - style: theme.textTheme.titleLarge, - ), - ), - const SizedBox(height: 12), - TextFormField( - decoration: InputDecoration( - isDense: true, - border: const OutlineInputBorder(), - labelText: widget.textHint ?? l10n.message(0), - ), - autofocus: true, - controller: messageController, - maxLines: 4, - ), - const SizedBox(height: 12), - if (widget.errorMessage != null) - Text( - widget.errorMessage!, - style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.error), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(l10n.cancel), - ), - const SizedBox(width: 8), - FilledButton( - onPressed: widget.errorMessage != null ? null : () => widget.onSubmit(messageController.text), - child: Text(widget.submitLabel ?? l10n.submit), - ) - ], - ), - const SizedBox(height: 16.0), - ], - ), - ); - } -} diff --git a/lib/post/widgets/share_post_action_bottom_sheet.dart b/lib/post/widgets/share_post_action_bottom_sheet.dart new file mode 100644 index 000000000..cfb23e963 --- /dev/null +++ b/lib/post/widgets/share_post_action_bottom_sheet.dart @@ -0,0 +1,188 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:go_router/go_router.dart'; +import 'package:share_plus/share_plus.dart'; + +import 'package:thunder/core/enums/media_type.dart'; +import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/core/singletons/lemmy_client.dart'; +import 'package:thunder/instance/bloc/instance_bloc.dart'; +import 'package:thunder/post/enums/post_action.dart'; +import 'package:thunder/post/utils/comment_action_helpers.dart'; +import 'package:thunder/shared/advanced_share_sheet.dart'; +import 'package:thunder/shared/bottom_sheet_action.dart'; +import 'package:thunder/shared/snackbar.dart'; + +/// Defines the actions that can be taken on a post when sharing +enum SharePostAction { + sharePost(icon: Icons.share_rounded, permissionType: PermissionType.user, requiresAuthentication: false), + sharePostLocal(icon: Icons.share_rounded, permissionType: PermissionType.user, requiresAuthentication: false), + shareImage(icon: Icons.image_rounded, permissionType: PermissionType.user, requiresAuthentication: false), + shareMedia(icon: Icons.personal_video_rounded, permissionType: PermissionType.user, requiresAuthentication: false), + shareLink(icon: Icons.link_rounded, permissionType: PermissionType.user, requiresAuthentication: false), + shareAdvanced(icon: Icons.screen_share_rounded, permissionType: PermissionType.user, requiresAuthentication: false), + ; + + String get name => switch (this) { + SharePostAction.sharePost => l10n.sharePost, + SharePostAction.sharePostLocal => l10n.sharePostLocal, + SharePostAction.shareImage => l10n.shareImage, + SharePostAction.shareMedia => l10n.shareMediaLink, + SharePostAction.shareLink => l10n.shareLink, + SharePostAction.shareAdvanced => l10n.advanced, + }; + + /// The icon to use for the action + final IconData icon; + + /// The permission type to use for the action + final PermissionType permissionType; + + /// Whether or not the action requires user authentication + final bool requiresAuthentication; + + const SharePostAction({required this.icon, required this.permissionType, required this.requiresAuthentication}); +} + +/// A bottom sheet that allows the user to perform actions on a instance. +/// +/// Given a [postViewMedia] and a [onAction] callback, this widget will display a list of actions that can be taken on the instance. +class SharePostActionBottomSheet extends StatefulWidget { + const SharePostActionBottomSheet({super.key, required this.context, required this.postViewMedia, required this.onAction}); + + /// The parent context + final BuildContext context; + + /// The post information + final PostViewMedia postViewMedia; + + /// Called when an action is selected + final Function() onAction; + + @override + State createState() => _SharePostActionBottomSheetState(); +} + +class _SharePostActionBottomSheetState extends State { + void retrieveMedia(String? url) async { + if (url == null) return; + + try { + // Try to get the cached image first + var media = await DefaultCacheManager().getFileFromCache(url); + File? mediaFile = media?.file; + + if (media == null) { + showSnackbar(l10n.downloadingMedia); + mediaFile = await DefaultCacheManager().getSingleFile(url); + } + + await Share.shareXFiles([XFile(mediaFile!.path)]); + } catch (e) { + showSnackbar(l10n.errorDownloadingMedia(e)); + } + } + + void performAction(SharePostAction action) { + switch (action) { + case SharePostAction.sharePost: + Share.share(widget.postViewMedia.postView.post.apId); + break; + case SharePostAction.sharePostLocal: + Share.share(LemmyClient.instance.generatePostUrl(widget.postViewMedia.postView.post.id)); + break; + case SharePostAction.shareImage: + retrieveMedia(widget.postViewMedia.media.first.imageUrl!); + break; + case SharePostAction.shareMedia: + Share.share(widget.postViewMedia.media.first.mediaUrl!); + break; + case SharePostAction.shareLink: + if (widget.postViewMedia.media.first.originalUrl != null) Share.share(widget.postViewMedia.media.first.originalUrl!); + break; + case SharePostAction.shareAdvanced: + showAdvancedShareSheet(widget.context, widget.postViewMedia); + break; + default: + break; + } + } + + String? generateSubtitle(SharePostAction action) { + PostViewMedia postViewMedia = widget.postViewMedia; + + switch (action) { + case SharePostAction.sharePost: + return postViewMedia.postView.post.apId; + case SharePostAction.sharePostLocal: + return LemmyClient.instance.generatePostUrl(postViewMedia.postView.post.id); + case SharePostAction.shareImage: + return postViewMedia.media.first.imageUrl; + case SharePostAction.shareMedia: + return postViewMedia.media.first.mediaUrl; + case SharePostAction.shareLink: + return postViewMedia.media.first.originalUrl; + case SharePostAction.shareAdvanced: + return l10n.useAdvancedShareSheet; + default: + return null; + } + } + + @override + Widget build(BuildContext context) { + List userActions = SharePostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); + + // Remove the share link option if there is no link or if the media link is the same as the external link + if (widget.postViewMedia.media.isEmpty || + widget.postViewMedia.media.first.mediaType == MediaType.text || + widget.postViewMedia.media.first.originalUrl == widget.postViewMedia.media.first.imageUrl || + widget.postViewMedia.media.first.originalUrl == widget.postViewMedia.media.first.mediaUrl) { + userActions.removeWhere((action) => action == SharePostAction.shareLink); + } + + // Remove the share image option if there is no image + if (widget.postViewMedia.media.isEmpty || widget.postViewMedia.media.first.imageUrl?.isNotEmpty != true) { + userActions.removeWhere((action) => action == SharePostAction.shareImage); + } + + // Remove the share media option if there is no media + if (widget.postViewMedia.media.isEmpty || widget.postViewMedia.media.first.mediaUrl?.isNotEmpty != true) { + userActions.removeWhere((action) => action == SharePostAction.shareMedia); + } + + // Remove the share local option if it is the same as the original + if (widget.postViewMedia.postView.post.apId == LemmyClient.instance.generatePostUrl(widget.postViewMedia.postView.post.id)) { + userActions.removeWhere((action) => action == SharePostAction.sharePostLocal); + } + + return BlocListener( + listener: (context, state) { + if (state.status == InstanceStatus.success) { + context.pop(); + widget.onAction(); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...userActions + .map( + (sharePostAction) => BottomSheetAction( + leading: Icon(sharePostAction.icon), + trailing: sharePostAction == SharePostAction.shareAdvanced ? const Icon(Icons.chevron_right_rounded) : null, + subtitle: generateSubtitle(sharePostAction), + title: sharePostAction.name, + onTap: () => performAction(sharePostAction), + ), + ) + .toList() as List, + ], + ), + ); + } +} diff --git a/lib/post/widgets/user_post_action_bottom_sheet.dart b/lib/post/widgets/user_post_action_bottom_sheet.dart index 4249491e5..a1e9fe777 100644 --- a/lib/post/widgets/user_post_action_bottom_sheet.dart +++ b/lib/post/widgets/user_post_action_bottom_sheet.dart @@ -16,11 +16,12 @@ import 'package:thunder/thunder/thunder_icons.dart'; import 'package:thunder/user/bloc/user_bloc.dart'; import 'package:thunder/user/enums/user_action.dart'; -// Defines the actions that can be taken on a user +/// Defines the actions that can be taken on a user +/// TODO: Implement admin-level actions enum UserPostAction { - viewProfile(icon: Icons.person, permissionType: PermissionType.user, requiresAuthentication: false), - blockUser(icon: Icons.block, permissionType: PermissionType.user, requiresAuthentication: true), - unblockUser(icon: Icons.block, permissionType: PermissionType.user, requiresAuthentication: true), + viewProfile(icon: Icons.person_search_rounded, permissionType: PermissionType.user, requiresAuthentication: false), + blockUser(icon: Icons.block_rounded, permissionType: PermissionType.user, requiresAuthentication: true), + unblockUser(icon: Icons.block_rounded, permissionType: PermissionType.user, requiresAuthentication: true), banUserFromCommunity(icon: Icons.block, permissionType: PermissionType.moderator, requiresAuthentication: true), unbanUserFromCommunity(icon: Icons.block, permissionType: PermissionType.moderator, requiresAuthentication: true), addUserAsCommunityModerator(icon: Icons.person_add_rounded, permissionType: PermissionType.moderator, requiresAuthentication: true), @@ -133,12 +134,16 @@ class _UserPostActionBottomSheetState extends State { final isUserBlocked = blockedUsers.where((personBlockView) => personBlockView.person.actorId == widget.postViewMedia.postView.creator.actorId).isNotEmpty; final isUserCommunityModerator = widget.postViewMedia.postView.creatorIsModerator ?? false; final isUserBannedFromCommunity = widget.postViewMedia.postView.creatorBannedFromCommunity; - final isUserBannedFromInstance = widget.postViewMedia.postView.creator.banned; - final isUserAdmin = widget.postViewMedia.postView.creatorIsAdmin ?? false; + // final isUserBannedFromInstance = widget.postViewMedia.postView.creator.banned; + // final isUserAdmin = widget.postViewMedia.postView.creatorIsAdmin ?? false; if (!isLoggedIn) { userActions = userActions.where((action) => action.requiresAuthentication == false).toList(); } else { + if (account?.actorId == widget.postViewMedia.postView.creator.actorId) { + userActions = userActions.where((action) => action != UserPostAction.blockUser && action != UserPostAction.unblockUser).toList(); + } + if (isUserBlocked) { userActions = userActions.where((action) => action != UserPostAction.blockUser).toList(); } else { From cfe4eded7dfd81a22033b7c9b0044293192a54ef Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Fri, 27 Sep 2024 14:26:21 -0700 Subject: [PATCH 3/9] feat: fix post submenu showing for guest accounts, add localizations for user actions, general code cleanup --- lib/l10n/app_en.arb | 20 +++++++ lib/post/utils/post.dart | 2 +- .../general_post_action_bottom_sheet.dart | 20 ++++--- .../user_post_action_bottom_sheet.dart | 59 ++++++++++--------- 4 files changed, 64 insertions(+), 37 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 435dbe82c..71ae2622f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -53,6 +53,10 @@ "@addAccountToSeeProfile": {}, "addAnonymousInstance": "Add Anonymous Instance", "@addAnonymousInstance": {}, + "addAsCommunityModerator": "Add as Community Moderator", + "@addAsCommunityModerator": { + "description": "Moderator action to add a user as community moderator" + }, "addDiscussionLanguage": "Add Language", "@addDiscussionLanguage": { "description": "Hint for text field to add language" @@ -147,6 +151,10 @@ "@backgroundCheckWarning": { "description": "Warning for enabling notifications" }, + "banFromCommunity": "Ban from Community", + "@banFromCommunity": { + "description": "Moderator action to ban a user from a community" + }, "bannedUser": "Banned User", "@bannedUser": { "description": "Short decription for moderator action to ban a user" @@ -1745,6 +1753,10 @@ "@remove": {}, "removeAccount": "Remove Account", "@removeAccount": {}, + "removeAsCommunityModerator": "Remove as Community Moderator", + "@removeAsCommunityModerator": { + "description": "Moderator action to remove user as community moderator" + }, "removeFromFavorites": "Remove from favorites", "@removeFromFavorites": { "description": "Action to remove a community in drawer from favorites" @@ -2469,6 +2481,10 @@ "@unableToRetrieveChangelog": { "description": "Error message for when we are unable to retrieve the changelog." }, + "unbanFromCommunity": "Unban from Community", + "@unbanFromCommunity": { + "description": "Moderator action to unban a user from a community" + }, "unbannedUser": "Unbanned User", "@unbannedUser": { "description": "Short decription for moderator action to unban a user" @@ -2485,6 +2501,10 @@ "@unblockInstance": { "description": "Tooltip for unblocking an instance" }, + "unblockUser": "Unblock User", + "@unblockUser": { + "description": "Action to unblock a user" + }, "understandEnable": "I Understand, Enable", "@understandEnable": { "description": "Action for acknowledging and enabling something" diff --git a/lib/post/utils/post.dart b/lib/post/utils/post.dart index 51d5d0483..d3a0815b6 100644 --- a/lib/post/utils/post.dart +++ b/lib/post/utils/post.dart @@ -243,7 +243,7 @@ Future removePost(int postId, bool remove, String reason) async { return postResponse.postView.post.removed == remove; } -/// Logic to remove a post to a community (moderator action) +/// Logic to report a given post Future reportPost(int postId, String reason) async { final l10n = AppLocalizations.of(GlobalContext.context)!; final account = await fetchActiveProfileAccount(); diff --git a/lib/post/widgets/general_post_action_bottom_sheet.dart b/lib/post/widgets/general_post_action_bottom_sheet.dart index 523b11346..26ab4be77 100644 --- a/lib/post/widgets/general_post_action_bottom_sheet.dart +++ b/lib/post/widgets/general_post_action_bottom_sheet.dart @@ -189,25 +189,32 @@ class _GeneralPostActionBottomSheetPageState extends State().state; final isLoggedIn = authState.isLoggedIn; - List userActions = GeneralQuickPostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); + List quickActions = GeneralQuickPostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); if (!isLoggedIn) { - userActions = userActions.where((action) => action.requiresAuthentication == false).toList(); + quickActions = quickActions.where((action) => action.requiresAuthentication == false).toList(); } else { // Hide hidden if instance does not support it if (!LemmyClient.instance.supportsFeature(LemmyFeature.hidePosts)) { - userActions = userActions.where((action) => action != GeneralQuickPostAction.hide).toList(); + quickActions = quickActions.where((action) => action != GeneralQuickPostAction.hide).toList(); } // Hide downvoted if instance does not support it if (!authState.downvotesEnabled) { - userActions = userActions.where((action) => action != GeneralQuickPostAction.downvote).toList(); + quickActions = quickActions.where((action) => action != GeneralQuickPostAction.downvote).toList(); } } + // Determine the available sub-menus to display + List submenus = GeneralPostAction.values.where((page) => page != GeneralPostAction.general).toList(); + + if (!isLoggedIn) { + submenus = submenus.where((action) => action != GeneralPostAction.post).toList(); + } + return Column( children: [ - if (userActions.isNotEmpty) + if (quickActions.isNotEmpty) MultiPickerItem( pickerItems: GeneralQuickPostAction.values .map((generalQuickPostAction) => PickerItemData( @@ -218,8 +225,7 @@ class _GeneralPostActionBottomSheetPageState extends State page != GeneralPostAction.general) + ...submenus .map( (page) => BottomSheetAction( leading: Icon(page.icon), diff --git a/lib/post/widgets/user_post_action_bottom_sheet.dart b/lib/post/widgets/user_post_action_bottom_sheet.dart index a1e9fe777..9082071d1 100644 --- a/lib/post/widgets/user_post_action_bottom_sheet.dart +++ b/lib/post/widgets/user_post_action_bottom_sheet.dart @@ -36,11 +36,11 @@ enum UserPostAction { String get name => switch (this) { UserPostAction.viewProfile => l10n.visitUserProfile, UserPostAction.blockUser => l10n.blockUser, - UserPostAction.unblockUser => "Unblock User", - UserPostAction.banUserFromCommunity => "Ban From Community", - UserPostAction.unbanUserFromCommunity => "Unban From Community", - UserPostAction.addUserAsCommunityModerator => "Add As Community Moderator", - UserPostAction.removeUserAsCommunityModerator => "Remove As Community Moderator", + UserPostAction.unblockUser => l10n.unblockUser, + UserPostAction.banUserFromCommunity => l10n.banFromCommunity, + UserPostAction.unbanUserFromCommunity => l10n.unbanFromCommunity, + UserPostAction.addUserAsCommunityModerator => l10n.addAsCommunityModerator, + UserPostAction.removeUserAsCommunityModerator => l10n.removeAsCommunityModerator, // UserPostAction.banUser => "Ban From Instance", // UserPostAction.unbanUser => "Unban User From Instance", // UserPostAction.purgeUser => "Purge User", @@ -122,11 +122,12 @@ class _UserPostActionBottomSheetState extends State { List userActions = UserPostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); List moderatorActions = UserPostAction.values.where((element) => element.permissionType == PermissionType.moderator).toList(); - List adminActions = UserPostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); + // List adminActions = UserPostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); final account = authState.getSiteResponse?.myUser?.localUserView.person; - final isModerator = authState.getSiteResponse?.myUser?.moderates.where((communityModeratorView) => communityModeratorView.moderator.actorId == account?.actorId).isNotEmpty ?? false; - final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; + final moderatedCommunities = authState.getSiteResponse?.myUser?.moderates ?? []; + final isModerator = moderatedCommunities.where((communityModeratorView) => communityModeratorView.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty; + // final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; final isLoggedIn = authState.isLoggedIn; final blockedUsers = authState.getSiteResponse?.myUser?.personBlocks ?? []; @@ -157,7 +158,7 @@ class _UserPostActionBottomSheetState extends State { moderatorActions = moderatorActions.where((action) => action != UserPostAction.removeUserAsCommunityModerator).toList(); } - if (!isUserBannedFromCommunity) { + if (isUserBannedFromCommunity) { moderatorActions = moderatorActions.where((action) => action != UserPostAction.banUserFromCommunity).toList(); } else { moderatorActions = moderatorActions.where((action) => action != UserPostAction.unbanUserFromCommunity).toList(); @@ -215,26 +216,26 @@ class _UserPostActionBottomSheetState extends State { ) .toList() as List, ], - if (isAdmin && adminActions.isNotEmpty) ...[ - const ThunderDivider(sliver: false, padding: false), - ...adminActions - .map( - (userPostAction) => BottomSheetAction( - leading: Icon(userPostAction.icon), - trailing: Padding( - padding: const EdgeInsets.only(left: 1), - child: Icon( - Thunder.shield_crown, - size: 20, - color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), - ), - ), - title: userPostAction.name, - onTap: () => performAction(userPostAction), - ), - ) - .toList() as List, - ], + // if (isAdmin && adminActions.isNotEmpty) ...[ + // const ThunderDivider(sliver: false, padding: false), + // ...adminActions + // .map( + // (userPostAction) => BottomSheetAction( + // leading: Icon(userPostAction.icon), + // trailing: Padding( + // padding: const EdgeInsets.only(left: 1), + // child: Icon( + // Thunder.shield_crown, + // size: 20, + // color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), + // ), + // ), + // title: userPostAction.name, + // onTap: () => performAction(userPostAction), + // ), + // ) + // .toList() as List, + // ], ], ), ); From 0f0750150f3b519b60cedd8ab3ac18d135a3166c Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Fri, 27 Sep 2024 14:42:29 -0700 Subject: [PATCH 4/9] lint: add comments, remove unnecessary code --- .../community_post_action_bottom_sheet.dart | 51 ++-- .../widgets/post_action_bottom_sheet.dart | 236 +----------------- lib/shared/bottom_sheet_action.dart | 3 + lib/user/utils/user.dart | 1 + 4 files changed, 31 insertions(+), 260 deletions(-) diff --git a/lib/post/widgets/community_post_action_bottom_sheet.dart b/lib/post/widgets/community_post_action_bottom_sheet.dart index 8b0010ace..03210980f 100644 --- a/lib/post/widgets/community_post_action_bottom_sheet.dart +++ b/lib/post/widgets/community_post_action_bottom_sheet.dart @@ -29,7 +29,7 @@ enum CommunityPostAction { CommunityPostAction.subscribeToCommunity => l10n.subscribeToCommunity, CommunityPostAction.unsubscribeFromCommunity => l10n.unsubscribeFromCommunity, CommunityPostAction.blockCommunity => l10n.blockCommunity, - CommunityPostAction.unblockCommunity => "Unblock Community", + CommunityPostAction.unblockCommunity => l10n.unblockCommunity, }; /// The icon to use for the action @@ -90,11 +90,12 @@ class _CommunityPostActionBottomSheetState extends State userActions = CommunityPostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); List moderatorActions = CommunityPostAction.values.where((element) => element.permissionType == PermissionType.moderator).toList(); - List adminActions = CommunityPostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); + // List adminActions = CommunityPostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); - final account = authState.getSiteResponse?.myUser?.localUserView.person; - final isModerator = authState.getSiteResponse?.myUser?.moderates.where((communityModeratorView) => communityModeratorView.moderator.actorId == account?.actorId).isNotEmpty ?? false; - final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; + // final account = authState.getSiteResponse?.myUser?.localUserView.person; + final moderatedCommunities = authState.getSiteResponse?.myUser?.moderates ?? []; + final isModerator = moderatedCommunities.where((communityModeratorView) => communityModeratorView.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty; + // final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; final isLoggedIn = authState.isLoggedIn; final blockedCommunities = authState.getSiteResponse?.myUser?.communityBlocks ?? []; @@ -158,26 +159,26 @@ class _CommunityPostActionBottomSheetState extends State, ], - if (isAdmin && adminActions.isNotEmpty) ...[ - const ThunderDivider(sliver: false, padding: false), - ...adminActions - .map( - (communityPostAction) => BottomSheetAction( - leading: Icon(communityPostAction.icon), - trailing: Padding( - padding: const EdgeInsets.only(left: 1), - child: Icon( - Thunder.shield_crown, - size: 20, - color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), - ), - ), - title: communityPostAction.name, - onTap: () => performAction(communityPostAction), - ), - ) - .toList() as List, - ], + // if (isAdmin && adminActions.isNotEmpty) ...[ + // const ThunderDivider(sliver: false, padding: false), + // ...adminActions + // .map( + // (communityPostAction) => BottomSheetAction( + // leading: Icon(communityPostAction.icon), + // trailing: Padding( + // padding: const EdgeInsets.only(left: 1), + // child: Icon( + // Thunder.shield_crown, + // size: 20, + // color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), + // ), + // ), + // title: communityPostAction.name, + // onTap: () => performAction(communityPostAction), + // ), + // ) + // .toList() as List, + // ], ], ), ); diff --git a/lib/post/widgets/post_action_bottom_sheet.dart b/lib/post/widgets/post_action_bottom_sheet.dart index 684682868..7c8123185 100644 --- a/lib/post/widgets/post_action_bottom_sheet.dart +++ b/lib/post/widgets/post_action_bottom_sheet.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter/material.dart'; +import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:lemmy_api_client/v3.dart'; @@ -163,237 +163,3 @@ class _PostActionBottomSheetState extends State { ); } } - - -// final List postCardActionItems = [ -// PostActionBottomSheet( -// postCardAction: PostCardAction.delete, -// icon: Icons.delete_rounded, -// label: l10n.delete, -// getOverrideIcon: (postView) => postView.post.deleted ? Icons.restore_from_trash_rounded : Icons.delete_rounded, -// getOverrideLabel: (context, postView) => postView.post.deleted ? l10n.restore : l10n.delete, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.moderatorLockPost, -// icon: Icons.lock, -// label: l10n.lockPost, -// getOverrideIcon: (postView) => postView.post.locked ? Icons.lock_open_rounded : Icons.lock, -// getOverrideLabel: (context, postView) => postView.post.locked ? l10n.unlockPost : l10n.lockPost, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.moderatorPinCommunity, -// icon: Icons.push_pin_rounded, -// label: l10n.pinToCommunity, -// getOverrideIcon: (postView) => postView.post.featuredCommunity ? Icons.push_pin_rounded : Icons.push_pin_outlined, -// getOverrideLabel: (context, postView) => postView.post.featuredCommunity ? l10n.unpinFromCommunity : l10n.pinToCommunity, -// ), -// PostActionBottomSheet( -// postCardAction: PostCardAction.moderatorRemovePost, -// icon: Icons.delete_forever_rounded, -// label: l10n.removePost, -// getOverrideIcon: (postView) => postView.post.removed ? Icons.restore_from_trash_rounded : Icons.delete_forever_rounded, -// getOverrideLabel: (context, postView) => postView.post.removed ? l10n.restorePost : l10n.removePost, -// ) -// ]; - - -// void showPostActionBottomModalSheet( -// BuildContext context, -// PostViewMedia postViewMedia, { -// PostActionBottomSheetPage page = PostActionBottomSheetPage.general, -// void Function(int userId)? onBlockedUser, -// void Function(int userId)? onBlockedCommunity, -// void Function(int postId)? onPostHidden, -// }) { - -// // Add the moderator actions submenu -// if (isModerator) { -// defaultPostCardActions.add(postCardActionItems.firstWhere((PostActionBottomSheet extendedPostCardActions) => extendedPostCardActions.postCardAction == PostCardAction.moderatorActions)); -// } - - -// // Generate the list of moderator actions -// final List moderatorPostCardActions = postCardActionItems -// .where((extendedAction) => [ -// PostCardAction.moderatorLockPost, -// PostCardAction.moderatorPinCommunity, -// PostCardAction.moderatorRemovePost, -// ].contains(extendedAction.postCardAction)) -// .toList(); - - -// // Generate the list of community actions -// final List communityActions = postCardActionItems -// .where((extendedAction) => [ -// PostCardAction.visitCommunity, -// postViewMedia.postView.subscribed == SubscribedType.notSubscribed ? PostCardAction.subscribeToCommunity : PostCardAction.unsubscribeFromCommunity, -// PostCardAction.blockCommunity, -// ].contains(extendedAction.postCardAction)) -// .toList(); - -// // Hide the option to block a community if the user is subscribed to it -// if (communityActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockCommunity) && postViewMedia.postView.subscribed != SubscribedType.notSubscribed) { -// communityActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockCommunity); -// } - -// // Generate the list of instance actions -// final List instanceActions = postCardActionItems -// .where((extendedAction) => [ -// PostCardAction.visitCommunityInstance, -// PostCardAction.blockCommunityInstance, -// PostCardAction.visitUserInstance, -// PostCardAction.blockUserInstance, -// ].contains(extendedAction.postCardAction)) -// .toList(); - -// // Remove block if unsupported -// if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockCommunityInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { -// instanceActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockCommunityInstance); -// } -// if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockUserInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { -// instanceActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockUserInstance); -// } - -// // Hide user block if user's instance is the same as the community' sinstance -// bool areSameInstance = areCommunityAndUserOnSameInstance(postViewMedia.postView); -// if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.visitUserInstance) && areSameInstance) { -// instanceActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.visitUserInstance); -// } -// if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockUserInstance) && areSameInstance) { -// instanceActions.removeWhere((PostActionBottomSheet postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockUserInstance); -// } - -// showModalBottomSheet( -// showDragHandle: true, -// isScrollControlled: true, -// context: context, -// builder: (builderContext) => PostCardActionPicker( -// postViewMedia: postViewMedia, -// page: page, -// postCardActions: { -// PostActionBottomSheetPage.general: defaultPostCardActions, -// PostActionBottomSheetPage.moderator: moderatorPostCardActions, -// PostActionBottomSheetPage.share: sharePostCardActions, -// PostActionBottomSheetPage.user: userActions, -// PostActionBottomSheetPage.community: communityActions, -// PostActionBottomSheetPage.instance: instanceActions, -// }, -// multiPostCardActions: {PostActionBottomSheetPage.general: defaultMultiPostCardActions}, -// titles: { -// PostActionBottomSheetPage.general: l10n.actions, -// PostActionBottomSheetPage.moderator: l10n.moderatorActions, -// PostActionBottomSheetPage.share: l10n.share, -// PostActionBottomSheetPage.user: l10n.userActions, -// PostActionBottomSheetPage.community: l10n.communityActions, -// PostActionBottomSheetPage.instance: l10n.instanceActions, -// }, -// outerContext: context, -// onBlockedUser: onBlockedUser, -// onBlockedCommunity: onBlockedCommunity, -// onPostHidden: onPostHidden, -// ), -// ); -// } - - -// class _PostCardActionPickerState extends State { -// PostActionBottomSheetPage? page; - -// @override -// Widget build(BuildContext context) { -// final ThemeData theme = Theme.of(context); -// final bool isUserLoggedIn = context.read().state.isLoggedIn; - -// return SingleChildScrollView( -// child: AnimatedSize( -// duration: const Duration(milliseconds: 100), -// curve: Curves.easeInOut, -// child: SingleChildScrollView( -// child: Column( -// mainAxisAlignment: MainAxisAlignment.start, -// mainAxisSize: MainAxisSize.max, -// children: [ -// // Post metadata chips -// if ((page ?? PostActionBottomSheetPage.general) == PostActionBottomSheetPage.general) -// Row( -// children: [ -// const SizedBox(width: 20), -// LanguagePostCardMetaData(languageId: widget.postViewMedia.postView.post.languageId), -// ], -// ), -// if (widget.multiPostCardActions[page ?? widget.page]?.isNotEmpty == true) -// MultiPickerItem( -// pickerItems: [ -// ...widget.multiPostCardActions[page ?? widget.page]!.where((a) => a.shouldShow?.call(context, widget.postViewMedia.postView) ?? true).map( -// (a) { -// return PickerItemData( -// label: a.getOverrideLabel?.call(context, widget.postViewMedia.postView) ?? a.label, -// icon: a.getOverrideIcon?.call(widget.postViewMedia.postView) ?? a.icon, -// backgroundColor: a.getColor?.call(context), -// foregroundColor: a.getForegroundColor?.call(context, widget.postViewMedia.postView), -// onSelected: (a.shouldEnable?.call(isUserLoggedIn) ?? true) ? () => onSelected(a.postCardAction) : null, -// ); -// }, -// ), -// ], -// ), - -// ], -// ), -// ), -// ), -// ); -// } - -// void onSelected(PostCardAction postCardAction) async { -// switch (postCardAction) { -// case PostCardAction.upvote: -// action = () => widget.outerContext -// .read() -// .add(FeedItemActionedEvent(postAction: PostAction.vote, postId: widget.postViewMedia.postView.post.id, value: widget.postViewMedia.postView.myVote == 1 ? 0 : 1)); -// break; -// case PostCardAction.downvote: -// action = () => widget.outerContext -// .read() -// .add(FeedItemActionedEvent(postAction: PostAction.vote, postId: widget.postViewMedia.postView.post.id, value: widget.postViewMedia.postView.myVote == -1 ? 0 : -1)); -// break; -// case PostCardAction.save: -// action = () => -// widget.outerContext.read().add(FeedItemActionedEvent(postAction: PostAction.save, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.saved)); -// break; -// case PostCardAction.toggleRead: -// action = () => -// widget.outerContext.read().add(FeedItemActionedEvent(postAction: PostAction.read, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.read)); -// break; -// case PostCardAction.hide: -// action = () => widget.outerContext -// .read() -// .add(FeedItemActionedEvent(postAction: PostAction.hide, postId: widget.postViewMedia.postView.post.id, value: !(widget.postViewMedia.postView.hidden ?? false))); -// widget.onPostHidden?.call(widget.postViewMedia.postView.post.id); -// break; -// case PostCardAction.delete: -// action = () => widget.outerContext -// .read() -// .add(FeedItemActionedEvent(postAction: PostAction.delete, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.post.deleted)); -// break; -// case PostCardAction.moderatorActions: -// action = () => setState(() => page = PostActionBottomSheetPage.moderator); -// pop = false; -// break; -// case PostCardAction.moderatorLockPost: -// action = () => widget.outerContext -// .read() -// .add(FeedItemActionedEvent(postAction: PostAction.lock, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.post.locked)); -// break; -// case PostCardAction.moderatorPinCommunity: -// action = () => widget.outerContext -// .read() -// .add(FeedItemActionedEvent(postAction: PostAction.pinCommunity, postId: widget.postViewMedia.postView.post.id, value: !widget.postViewMedia.postView.post.featuredCommunity)); -// break; -// case PostCardAction.moderatorRemovePost: -// action = () => showRemovePostReasonBottomSheet(widget.outerContext, widget.postViewMedia); -// break; -// } -// } -// } - diff --git a/lib/shared/bottom_sheet_action.dart b/lib/shared/bottom_sheet_action.dart index 42dbf679e..b36d864a1 100644 --- a/lib/shared/bottom_sheet_action.dart +++ b/lib/shared/bottom_sheet_action.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +/// Defines a widget that can be used in a [BottomSheet]. Can provide optional [leading] and [trailing] widgets. +/// +/// When tapped, will call the [onTap] callback. class BottomSheetAction extends StatelessWidget { const BottomSheetAction({super.key, required this.leading, this.trailing, required this.title, this.subtitle, required this.onTap}); diff --git a/lib/user/utils/user.dart b/lib/user/utils/user.dart index 66579bbc7..124c2a2bd 100644 --- a/lib/user/utils/user.dart +++ b/lib/user/utils/user.dart @@ -46,6 +46,7 @@ Future banUserFromCommunity(int userId, bool ban, {req return banFromCommunityResponse; } +/// Logic to add or remove moderator for a given community (moderator action) Future addModerator(int userId, bool added, {required int communityId}) async { final l10n = AppLocalizations.of(GlobalContext.context)!; final account = await fetchActiveProfileAccount(); From f20d8e286f783d478e5fe964e81684c0ae665b13 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Fri, 27 Sep 2024 15:05:44 -0700 Subject: [PATCH 5/9] lint: add additional localization strings, comment unused code --- lib/l10n/app_en.arb | 36 +++++++ .../general_post_action_bottom_sheet.dart | 6 +- .../instance_post_action_bottom_sheet.dart | 98 +++++++++---------- .../post_post_action_bottom_sheet.dart | 64 ++++++------ 4 files changed, 120 insertions(+), 84 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 71ae2622f..adb24a729 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -677,6 +677,10 @@ "@deleteLocalPreferencesDescription": { "description": "Description for confirmation action to delete local preferences" }, + "deletePost": "Delete Post", + "@deletePost": { + "description": "Action for deleting a post" + }, "deleteUserLabelConfirmation": "Are you sure you want to delete the label?", "@deleteUserLabelConfirmation": { "description": "Confirmation message for deleting a label" @@ -1581,6 +1585,10 @@ "@permissionDeniedMessage": { "description": "Explanation for error when user denies OS permissions" }, + "pinPostToCommunity": "Pin Post to Community", + "@pinPostToCommunity": { + "description": "Action for pinning a post to a community (moderator action)" + }, "pinToCommunity": "Pin to Community", "@pinToCommunity": { "description": "Setting for pinning a post to a community (moderator action)" @@ -1589,6 +1597,14 @@ "@placeholderText": { "description": "Placeholder text for any previews. This comes from https://www.lipsum.com/" }, + "post": "Post", + "@post": { + "description": "Describes a single post" + }, + "postActions": "Post Actions", + "@postActions": { + "description": "Describes a given set of actions that can be performed on a post" + }, "postBehaviourSettings": "Posts", "@postBehaviourSettings": { "description": "Subcategory in Setting -> General" @@ -1725,6 +1741,10 @@ }, "reachedTheBottom": "No more items to load", "@reachedTheBottom": {}, + "read": "Read", + "@read": { + "description": "Indicates that a post has been read" + }, "readAll": "Read All", "@readAll": {}, "reason": "Reason", @@ -1817,6 +1837,10 @@ "@report": {}, "reportComment": "Report Comment", "@reportComment": {}, + "reportPost": "Report Post", + "@reportPost": { + "description": "Action to report a post (moderator action)" + }, "reporter": "Reporter:", "@reporter": { "description": "Name of reporter that reported a post/comment" @@ -2497,6 +2521,10 @@ "@unblockCommunity": { "description": "Action to unblock a community" }, + "unblockCommunityInstance": "Unblock Community Instance", + "@unblockCommunityInstance": { + "description": "Action to unblock the instance of a given community" + }, "unblockInstance": "Unblock Instance", "@unblockInstance": { "description": "Tooltip for unblocking an instance" @@ -2505,6 +2533,10 @@ "@unblockUser": { "description": "Action to unblock a user" }, + "unblockUserInstance": "Unblock User Instance", + "@unblockUserInstance": { + "description": "Action to unblock the instance of a given user" + }, "understandEnable": "I Understand, Enable", "@understandEnable": { "description": "Action for acknowledging and enabling something" @@ -2551,6 +2583,10 @@ "@unpinFromCommunity": { "description": "Setting for unpinning a post from a community (moderator action)" }, + "unpinPostFromCommunity": "Unpin Post from Community", + "@unpinPostFromCommunity": { + "description": "Moderator action to unpin a post from a community" + }, "unreachable": "Unreachable", "@unreachable": { "description": "Describes an instance that is currently unreachable" diff --git a/lib/post/widgets/general_post_action_bottom_sheet.dart b/lib/post/widgets/general_post_action_bottom_sheet.dart index 26ab4be77..58a335330 100644 --- a/lib/post/widgets/general_post_action_bottom_sheet.dart +++ b/lib/post/widgets/general_post_action_bottom_sheet.dart @@ -24,7 +24,7 @@ enum GeneralPostAction { share(icon: Icons.share); String get name => switch (this) { - GeneralPostAction.post => "Post", + GeneralPostAction.post => l10n.post, GeneralPostAction.user => l10n.user, GeneralPostAction.community => l10n.community, GeneralPostAction.instance => l10n.instance(1), @@ -34,7 +34,7 @@ enum GeneralPostAction { /// The title to use for the action. This is shown when the given page is active String get title => switch (this) { - GeneralPostAction.post => "Post Actions", + GeneralPostAction.post => l10n.postActions, GeneralPostAction.user => l10n.userActions, GeneralPostAction.community => l10n.communityActions, GeneralPostAction.instance => l10n.instanceActions, @@ -160,7 +160,7 @@ class _GeneralPostActionBottomSheetPageState extends State switch (this) { InstancePostAction.visitCommunityInstance => l10n.visitCommunityInstance, InstancePostAction.blockCommunityInstance => l10n.blockCommunityInstance, - InstancePostAction.unblockCommunityInstance => "Unblock Community Instance", + InstancePostAction.unblockCommunityInstance => l10n.unblockCommunityInstance, InstancePostAction.visitUserInstance => l10n.visitUserInstance, InstancePostAction.blockUserInstance => l10n.blockUserInstance, - InstancePostAction.unblockUserInstance => "Unblock User Instance", + InstancePostAction.unblockUserInstance => l10n.unblockUserInstance, }; /// The icon to use for the action @@ -108,16 +108,16 @@ class _InstancePostActionBottomSheetState extends State().state; List userActions = InstancePostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); - List moderatorActions = InstancePostAction.values.where((element) => element.permissionType == PermissionType.moderator).toList(); - List adminActions = InstancePostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); + // List moderatorActions = InstancePostAction.values.where((element) => element.permissionType == PermissionType.moderator).toList(); + // List adminActions = InstancePostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); final account = authState.getSiteResponse?.myUser?.localUserView.person; - final isModerator = authState.getSiteResponse?.myUser?.moderates.where((communityModeratorView) => communityModeratorView.moderator.actorId == account?.actorId).isNotEmpty ?? false; - final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; + // final moderatedCommunities = authState.getSiteResponse?.myUser?.moderates ?? []; + // final isModerator = moderatedCommunities.where((communityModeratorView) => communityModeratorView.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty; + // final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; final isLoggedIn = authState.isLoggedIn; final blockedInstances = authState.getSiteResponse?.myUser?.instanceBlocks ?? []; @@ -184,46 +184,46 @@ class _InstancePostActionBottomSheetState extends State, - if (isModerator && moderatorActions.isNotEmpty) ...[ - const ThunderDivider(sliver: false, padding: false), - ...moderatorActions - .map( - (instancePostAction) => BottomSheetAction( - leading: Icon(instancePostAction.icon), - trailing: Padding( - padding: const EdgeInsets.only(left: 1), - child: Icon( - Thunder.shield, - size: 20, - color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.green), - ), - ), - title: instancePostAction.name, - onTap: () => performAction(instancePostAction), - ), - ) - .toList() as List, - ], - if (isAdmin && adminActions.isNotEmpty) ...[ - const ThunderDivider(sliver: false, padding: false), - ...adminActions - .map( - (instancePostAction) => BottomSheetAction( - leading: Icon(instancePostAction.icon), - trailing: Padding( - padding: const EdgeInsets.only(left: 1), - child: Icon( - Thunder.shield_crown, - size: 20, - color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), - ), - ), - title: instancePostAction.name, - onTap: () => performAction(instancePostAction), - ), - ) - .toList() as List, - ], + // if (isModerator && moderatorActions.isNotEmpty) ...[ + // const ThunderDivider(sliver: false, padding: false), + // ...moderatorActions + // .map( + // (instancePostAction) => BottomSheetAction( + // leading: Icon(instancePostAction.icon), + // trailing: Padding( + // padding: const EdgeInsets.only(left: 1), + // child: Icon( + // Thunder.shield, + // size: 20, + // color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.green), + // ), + // ), + // title: instancePostAction.name, + // onTap: () => performAction(instancePostAction), + // ), + // ) + // .toList() as List, + // ], + // if (isAdmin && adminActions.isNotEmpty) ...[ + // const ThunderDivider(sliver: false, padding: false), + // ...adminActions + // .map( + // (instancePostAction) => BottomSheetAction( + // leading: Icon(instancePostAction.icon), + // trailing: Padding( + // padding: const EdgeInsets.only(left: 1), + // child: Icon( + // Thunder.shield_crown, + // size: 20, + // color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), + // ), + // ), + // title: instancePostAction.name, + // onTap: () => performAction(instancePostAction), + // ), + // ) + // .toList() as List, + // ], ], ), ); diff --git a/lib/post/widgets/post_post_action_bottom_sheet.dart b/lib/post/widgets/post_post_action_bottom_sheet.dart index 87ebb9609..c3ea01d54 100644 --- a/lib/post/widgets/post_post_action_bottom_sheet.dart +++ b/lib/post/widgets/post_post_action_bottom_sheet.dart @@ -31,16 +31,16 @@ enum PostPostAction { ; String get name => switch (this) { - PostPostAction.reportPost => "Report Post", + PostPostAction.reportPost => l10n.reportPost, PostPostAction.editPost => l10n.editPost, - PostPostAction.deletePost => "Delete Post", + PostPostAction.deletePost => l10n.deletePost, PostPostAction.restorePost => l10n.restorePost, PostPostAction.lockPost => l10n.lockPost, PostPostAction.unlockPost => l10n.unlockPost, PostPostAction.removePost => l10n.removePost, - PostPostAction.restorePostAsModerator => "Restore Post", - PostPostAction.pinPostToCommunity => "Pin Post To Community", - PostPostAction.unpinPostFromCommunity => "Unpin Post From Community", + PostPostAction.restorePostAsModerator => l10n.restorePost, + PostPostAction.pinPostToCommunity => l10n.pinPostToCommunity, + PostPostAction.unpinPostFromCommunity => l10n.unpinPostFromCommunity, // PostPostAction.pinPostToInstance => "Pin Post To Instance", // PostPostAction.unpinPostFromInstance => "Unpin Post From Instance", }; @@ -133,8 +133,8 @@ class _PostPostActionBottomSheetState extends State { showThunderDialog( context: widget.context, - title: "Report Post", - primaryButtonText: "Report", + title: l10n.reportPost, + primaryButtonText: l10n.report(1), onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { widget.context.read().add( FeedItemActionedEvent( @@ -203,17 +203,17 @@ class _PostPostActionBottomSheetState extends State { List userActions = PostPostAction.values.where((element) => element.permissionType == PermissionType.user).toList(); List moderatorActions = PostPostAction.values.where((element) => element.permissionType == PermissionType.moderator).toList(); - List adminActions = PostPostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); + // List adminActions = PostPostAction.values.where((element) => element.permissionType == PermissionType.admin).toList(); final account = authState.getSiteResponse?.myUser?.localUserView.person; - final isModerator = - authState.getSiteResponse?.myUser?.moderates.where((communityModeratorView) => communityModeratorView.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty ?? false; - final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; + final moderatedCommunities = authState.getSiteResponse?.myUser?.moderates ?? []; + final isModerator = moderatedCommunities.where((communityModeratorView) => communityModeratorView.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty; + // final isAdmin = authState.getSiteResponse?.admins.where((personView) => personView.person.actorId == account?.actorId).isNotEmpty ?? false; final isLoggedIn = authState.isLoggedIn; final isPostLocked = widget.postViewMedia.postView.post.locked; final isPostPinnedToCommunity = widget.postViewMedia.postView.post.featuredCommunity; // Pin to community - final isPostPinnedToInstance = widget.postViewMedia.postView.post.featuredLocal; // Pin to instance + // final isPostPinnedToInstance = widget.postViewMedia.postView.post.featuredLocal; // Pin to instance final isPostDeleted = widget.postViewMedia.postView.post.deleted; // Deleted by the user final isPostRemoved = widget.postViewMedia.postView.post.removed; // Removed by a moderator @@ -289,26 +289,26 @@ class _PostPostActionBottomSheetState extends State { ) .toList() as List, ], - if (isAdmin && adminActions.isNotEmpty) ...[ - const ThunderDivider(sliver: false, padding: false), - ...adminActions - .map( - (postPostAction) => BottomSheetAction( - leading: Icon(postPostAction.icon), - trailing: Padding( - padding: const EdgeInsets.only(left: 1), - child: Icon( - Thunder.shield_crown, - size: 20, - color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), - ), - ), - title: postPostAction.name, - onTap: () => performAction(postPostAction), - ), - ) - .toList() as List, - ], + // if (isAdmin && adminActions.isNotEmpty) ...[ + // const ThunderDivider(sliver: false, padding: false), + // ...adminActions + // .map( + // (postPostAction) => BottomSheetAction( + // leading: Icon(postPostAction.icon), + // trailing: Padding( + // padding: const EdgeInsets.only(left: 1), + // child: Icon( + // Thunder.shield_crown, + // size: 20, + // color: Color.alphaBlend(theme.colorScheme.primary.withOpacity(0.4), Colors.red), + // ), + // ), + // title: postPostAction.name, + // onTap: () => performAction(postPostAction), + // ), + // ) + // .toList() as List, + // ], ], ); } From c5c0130ffd563e32d1dffca606fd43892ee8f620 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Sun, 29 Sep 2024 12:10:51 -0700 Subject: [PATCH 6/9] feat: add language metadata to post actions bottom sheet --- lib/post/widgets/post_action_bottom_sheet.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/post/widgets/post_action_bottom_sheet.dart b/lib/post/widgets/post_action_bottom_sheet.dart index 7c8123185..dacf967db 100644 --- a/lib/post/widgets/post_action_bottom_sheet.dart +++ b/lib/post/widgets/post_action_bottom_sheet.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:lemmy_api_client/v3.dart'; +import 'package:thunder/community/widgets/post_card_metadata.dart'; import 'package:thunder/core/enums/full_name.dart'; import 'package:thunder/core/models/post_view_media.dart'; @@ -154,6 +155,10 @@ class _PostActionBottomSheetState extends State { ), ], ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + child: LanguagePostCardMetaData(languageId: widget.postViewMedia.postView.post.languageId), + ), const SizedBox(height: 16.0), actions, ], From b7e109a1fbf757722ac81639876248e6a5c1e199 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Fri, 4 Oct 2024 12:48:51 -0700 Subject: [PATCH 7/9] feat: bring back post action callbacks --- lib/community/widgets/post_card.dart | 32 ++++++++++++-- .../widgets/post_card_view_comfortable.dart | 36 ++++++++++++++-- .../community_post_action_bottom_sheet.dart | 9 +++- .../general_post_action_bottom_sheet.dart | 6 ++- .../widgets/post_action_bottom_sheet.dart | 43 +++++++++++++------ .../post_post_action_bottom_sheet.dart | 2 +- lib/post/widgets/post_view.dart | 33 +++++++++++++- .../user_post_action_bottom_sheet.dart | 13 +++++- 8 files changed, 146 insertions(+), 28 deletions(-) diff --git a/lib/community/widgets/post_card.dart b/lib/community/widgets/post_card.dart index b8541a8d9..b8b133eea 100644 --- a/lib/community/widgets/post_card.dart +++ b/lib/community/widgets/post_card.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thunder/community/enums/community_action.dart'; import 'package:thunder/community/utils/post_actions.dart'; import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/community/widgets/post_card_view_comfortable.dart'; @@ -18,6 +19,7 @@ import 'package:thunder/feed/widgets/widgets.dart'; import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/post/utils/navigate_post.dart'; +import 'package:thunder/user/enums/user_action.dart'; class PostCard extends StatefulWidget { final PostViewMedia postViewMedia; @@ -263,9 +265,33 @@ class _PostCardState extends State { onLongPress: () => showPostActionBottomModalSheet( context, widget.postViewMedia, - // onBlockedUser: (userId) => context.read().add(FeedDismissBlockedEvent(userId: userId)), - // onBlockedCommunity: (communityId) => context.read().add(FeedDismissBlockedEvent(communityId: communityId)), - // onPostHidden: (postId) => context.read().add(FeedDismissHiddenPostEvent(postId: postId)), + onAction: ({postAction, userAction, communityAction, required postViewMedia}) async { + if (postAction == null && userAction == null && communityAction == null) return; + + switch (postAction) { + case PostAction.hide: + context.read().add(FeedDismissHiddenPostEvent(postId: postViewMedia.postView.post.id)); + break; + default: + break; + } + + switch (userAction) { + case UserAction.block: + context.read().add(FeedDismissBlockedEvent(userId: postViewMedia.postView.creator.id)); + break; + default: + break; + } + + switch (communityAction) { + case CommunityAction.block: + context.read().add(FeedDismissBlockedEvent(communityId: postViewMedia.postView.community.id)); + break; + default: + break; + } + }, ), onTap: () async { PostView postView = widget.postViewMedia.postView; diff --git a/lib/community/widgets/post_card_view_comfortable.dart b/lib/community/widgets/post_card_view_comfortable.dart index b52142638..656e1da8c 100644 --- a/lib/community/widgets/post_card_view_comfortable.dart +++ b/lib/community/widgets/post_card_view_comfortable.dart @@ -8,6 +8,8 @@ import 'package:lemmy_api_client/v3.dart'; import 'package:markdown/markdown.dart' hide Text; import 'package:thunder/account/bloc/account_bloc.dart'; +import 'package:thunder/community/enums/community_action.dart'; +import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/community/widgets/post_card_actions.dart'; import 'package:thunder/community/widgets/post_card_metadata.dart'; @@ -20,6 +22,7 @@ import 'package:thunder/feed/feed.dart'; import 'package:thunder/shared/media_view.dart'; import 'package:thunder/shared/text/scalable_text.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; +import 'package:thunder/user/enums/user_action.dart'; class PostCardViewComfortable extends StatelessWidget { final Function(int) onVoteAction; @@ -102,7 +105,7 @@ class PostCardViewComfortable extends StatelessWidget { final bool darkTheme = context.read().state.useDarkTheme; return Container( - color: indicateRead && postViewMedia.postView.read ? theme.colorScheme.onBackground.withOpacity(darkTheme ? 0.05 : 0.075) : null, + color: indicateRead && postViewMedia.postView.read ? theme.colorScheme.onSurface.withOpacity(darkTheme ? 0.05 : 0.075) : null, padding: const EdgeInsets.symmetric(vertical: 12.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -348,10 +351,35 @@ class PostCardViewComfortable extends StatelessWidget { showPostActionBottomModalSheet( context, postViewMedia, - // onBlockedUser: (userId) => context.read().add(FeedDismissBlockedEvent(userId: userId)), - // onBlockedCommunity: (communityId) => context.read().add(FeedDismissBlockedEvent(communityId: communityId)), - // onPostHidden: (postId) => context.read().add(FeedDismissHiddenPostEvent(postId: postId)), + onAction: ({postAction, userAction, communityAction, required postViewMedia}) async { + if (postAction == null && userAction == null && communityAction == null) return; + + switch (postAction) { + case PostAction.hide: + context.read().add(FeedDismissHiddenPostEvent(postId: postViewMedia.postView.post.id)); + break; + default: + break; + } + + switch (userAction) { + case UserAction.block: + context.read().add(FeedDismissBlockedEvent(userId: postViewMedia.postView.creator.id)); + break; + default: + break; + } + + switch (communityAction) { + case CommunityAction.block: + context.read().add(FeedDismissBlockedEvent(communityId: postViewMedia.postView.community.id)); + break; + default: + break; + } + }, ); + HapticFeedback.mediumImpact(); }), if (isUserLoggedIn) diff --git a/lib/post/widgets/community_post_action_bottom_sheet.dart b/lib/post/widgets/community_post_action_bottom_sheet.dart index 03210980f..f47100a8b 100644 --- a/lib/post/widgets/community_post_action_bottom_sheet.dart +++ b/lib/post/widgets/community_post_action_bottom_sheet.dart @@ -55,13 +55,15 @@ class CommunityPostActionBottomSheet extends StatefulWidget { final PostViewMedia postViewMedia; /// Called when an action is selected - final Function(CommunityView? communityView) onAction; + final Function(CommunityAction communityAction, CommunityView? communityView) onAction; @override State createState() => _CommunityPostActionBottomSheetState(); } class _CommunityPostActionBottomSheetState extends State { + CommunityAction? _communityAction; + void performAction(CommunityPostAction action) { switch (action) { case CommunityPostAction.viewCommunity: @@ -70,12 +72,14 @@ class _CommunityPostActionBottomSheetState extends State().add(CommunityActionEvent(communityId: widget.postViewMedia.postView.community.id, communityAction: CommunityAction.follow, value: true)); + setState(() => _communityAction = CommunityAction.follow); break; case CommunityPostAction.unsubscribeFromCommunity: context.read().add(CommunityActionEvent(communityId: widget.postViewMedia.postView.community.id, communityAction: CommunityAction.follow, value: false)); break; case CommunityPostAction.blockCommunity: context.read().add(CommunityActionEvent(communityId: widget.postViewMedia.postView.community.id, communityAction: CommunityAction.block, value: true)); + setState(() => _communityAction = CommunityAction.block); break; case CommunityPostAction.unblockCommunity: context.read().add(CommunityActionEvent(communityId: widget.postViewMedia.postView.community.id, communityAction: CommunityAction.block, value: false)); @@ -124,7 +128,8 @@ class _CommunityPostActionBottomSheetState extends State _communityAction = null); } }, child: Column( diff --git a/lib/post/widgets/general_post_action_bottom_sheet.dart b/lib/post/widgets/general_post_action_bottom_sheet.dart index 58a335330..108d83b72 100644 --- a/lib/post/widgets/general_post_action_bottom_sheet.dart +++ b/lib/post/widgets/general_post_action_bottom_sheet.dart @@ -74,7 +74,7 @@ enum GeneralQuickPostAction { /// Defines the general top-levelactions that can be taken on a post. /// Given a [postViewMedia] and a [onSwitchActivePage] callback, this widget will display a list of actions that can be taken on the post. class GeneralPostActionBottomSheetPage extends StatefulWidget { - const GeneralPostActionBottomSheetPage({super.key, required this.context, required this.postViewMedia, required this.onSwitchActivePage}); + const GeneralPostActionBottomSheetPage({super.key, required this.context, required this.postViewMedia, required this.onSwitchActivePage, required this.onAction}); /// The outer context final BuildContext context; @@ -85,6 +85,9 @@ class GeneralPostActionBottomSheetPage extends StatefulWidget { /// Called when the active page is changed final Function(GeneralPostAction page) onSwitchActivePage; + /// Called when an action is selected + final Function(PostAction postAction, PostViewMedia? postViewMedia) onAction; + @override State createState() => _GeneralPostActionBottomSheetPageState(); } @@ -126,6 +129,7 @@ class _GeneralPostActionBottomSheetPageState extends State().add(FeedItemActionedEvent(postAction: PostAction.hide, postId: postViewMedia.postView.post.id, value: postViewMedia.postView.hidden == true ? false : true)); + widget.onAction(PostAction.hide, postViewMedia); break; } diff --git a/lib/post/widgets/post_action_bottom_sheet.dart b/lib/post/widgets/post_action_bottom_sheet.dart index dacf967db..26ac1f630 100644 --- a/lib/post/widgets/post_action_bottom_sheet.dart +++ b/lib/post/widgets/post_action_bottom_sheet.dart @@ -5,8 +5,9 @@ import 'package:flutter/material.dart'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:lemmy_api_client/v3.dart'; -import 'package:thunder/community/widgets/post_card_metadata.dart'; +import 'package:thunder/community/enums/community_action.dart'; +import 'package:thunder/community/widgets/post_card_metadata.dart'; import 'package:thunder/core/enums/full_name.dart'; import 'package:thunder/core/models/post_view_media.dart'; import 'package:thunder/post/enums/post_action.dart'; @@ -16,6 +17,7 @@ import 'package:thunder/post/widgets/instance_post_action_bottom_sheet.dart'; import 'package:thunder/post/widgets/post_post_action_bottom_sheet.dart'; import 'package:thunder/post/widgets/share_post_action_bottom_sheet.dart'; import 'package:thunder/post/widgets/user_post_action_bottom_sheet.dart'; +import 'package:thunder/user/enums/user_action.dart'; import 'package:thunder/utils/instance.dart'; import 'package:thunder/utils/global_context.dart'; @@ -26,18 +28,18 @@ void showPostActionBottomModalSheet( BuildContext context, PostViewMedia postViewMedia, { GeneralPostAction page = GeneralPostAction.general, - void Function({PostAction? action})? onAction, + void Function({PostAction? postAction, UserAction? userAction, CommunityAction? communityAction, required PostViewMedia postViewMedia})? onAction, }) { showModalBottomSheet( context: context, showDragHandle: true, isScrollControlled: true, - builder: (_) => PostActionBottomSheet(context: context, postViewMedia: postViewMedia), + builder: (_) => PostActionBottomSheet(context: context, postViewMedia: postViewMedia, onAction: onAction), ); } class PostActionBottomSheet extends StatefulWidget { - const PostActionBottomSheet({super.key, required this.context, required this.postViewMedia, this.initialPage = GeneralPostAction.general}); + const PostActionBottomSheet({super.key, required this.context, required this.postViewMedia, this.initialPage = GeneralPostAction.general, required this.onAction}); /// The parent context final BuildContext context; @@ -48,6 +50,9 @@ class PostActionBottomSheet extends StatefulWidget { /// The initial page of the bottom sheet final GeneralPostAction initialPage; + /// The callback that is called when an action is performed + final void Function({PostAction? postAction, UserAction? userAction, CommunityAction? communityAction, required PostViewMedia postViewMedia})? onAction; + @override State createState() => _PostActionBottomSheetState(); } @@ -100,23 +105,32 @@ class _PostActionBottomSheetState extends State { final theme = Theme.of(context); Widget actions = switch (currentPage) { - GeneralPostAction.post => PostPostActionBottomSheet( + GeneralPostAction.general => GeneralPostActionBottomSheetPage( context: widget.context, postViewMedia: widget.postViewMedia, - onAction: () {}, + onSwitchActivePage: (page) => setState(() => currentPage = page), + onAction: (PostAction postAction, PostViewMedia? updatedPostViewMedia) { + widget.onAction?.call(postAction: postAction, postViewMedia: widget.postViewMedia); + }, ), - GeneralPostAction.general => GeneralPostActionBottomSheetPage( + GeneralPostAction.post => PostPostActionBottomSheet( context: widget.context, postViewMedia: widget.postViewMedia, - onSwitchActivePage: (page) => setState(() => currentPage = page), + onAction: (PostAction postAction, PostViewMedia? updatedPostViewMedia) { + widget.onAction?.call(postAction: postAction, postViewMedia: widget.postViewMedia); + }, ), GeneralPostAction.user => UserPostActionBottomSheet( postViewMedia: widget.postViewMedia, - onAction: (PersonView? updatedPersonView) {}, + onAction: (UserAction userAction, PersonView? updatedPersonView) { + widget.onAction?.call(userAction: userAction, postViewMedia: widget.postViewMedia); + }, ), GeneralPostAction.community => CommunityPostActionBottomSheet( postViewMedia: widget.postViewMedia, - onAction: (CommunityView? updatedCommunityView) {}, + onAction: (CommunityAction communityAction, CommunityView? updatedCommunityView) { + widget.onAction?.call(communityAction: communityAction, postViewMedia: widget.postViewMedia); + }, ), GeneralPostAction.instance => InstancePostActionBottomSheet( postViewMedia: widget.postViewMedia, @@ -155,10 +169,11 @@ class _PostActionBottomSheetState extends State { ), ], ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), - child: LanguagePostCardMetaData(languageId: widget.postViewMedia.postView.post.languageId), - ), + if (currentPage == GeneralPostAction.general) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + child: LanguagePostCardMetaData(languageId: widget.postViewMedia.postView.post.languageId), + ), const SizedBox(height: 16.0), actions, ], diff --git a/lib/post/widgets/post_post_action_bottom_sheet.dart b/lib/post/widgets/post_post_action_bottom_sheet.dart index c3ea01d54..702a2e52e 100644 --- a/lib/post/widgets/post_post_action_bottom_sheet.dart +++ b/lib/post/widgets/post_post_action_bottom_sheet.dart @@ -71,7 +71,7 @@ class PostPostActionBottomSheet extends StatefulWidget { final PostViewMedia postViewMedia; /// Called when an action is selected - final Function() onAction; + final Function(PostAction postAction, PostViewMedia? postViewMedia) onAction; @override State createState() => _PostPostActionBottomSheetState(); diff --git a/lib/post/widgets/post_view.dart b/lib/post/widgets/post_view.dart index 01e52ff4a..2031cfb59 100644 --- a/lib/post/widgets/post_view.dart +++ b/lib/post/widgets/post_view.dart @@ -17,7 +17,10 @@ import 'package:swipeable_page_route/swipeable_page_route.dart'; import 'package:thunder/account/bloc/account_bloc.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/comment/utils/navigate_comment.dart'; +import 'package:thunder/community/enums/community_action.dart'; import 'package:thunder/community/pages/create_post_page.dart'; +import 'package:thunder/feed/bloc/feed_bloc.dart'; +import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/post/widgets/general_post_action_bottom_sheet.dart'; import 'package:thunder/post/widgets/post_action_bottom_sheet.dart'; import 'package:thunder/community/widgets/post_card_type_badge.dart'; @@ -45,6 +48,7 @@ import 'package:thunder/shared/media_view.dart'; import 'package:thunder/shared/reply_to_preview_actions.dart'; import 'package:thunder/shared/text/scalable_text.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; +import 'package:thunder/user/enums/user_action.dart'; class PostSubview extends StatefulWidget { final PostViewMedia postViewMedia; @@ -298,8 +302,35 @@ class _PostSubviewState extends State with SingleTickerProviderStat onShare: () { showPostActionBottomModalSheet( context, - widget.postViewMedia, + postViewMedia, page: GeneralPostAction.share, + onAction: ({postAction, userAction, communityAction, required postViewMedia}) async { + if (postAction == null && userAction == null && communityAction == null) return; + + switch (postAction) { + case PostAction.hide: + context.read().add(FeedDismissHiddenPostEvent(postId: postViewMedia.postView.post.id)); + break; + default: + break; + } + + switch (userAction) { + case UserAction.block: + context.read().add(FeedDismissBlockedEvent(userId: postViewMedia.postView.creator.id)); + break; + default: + break; + } + + switch (communityAction) { + case CommunityAction.block: + context.read().add(FeedDismissBlockedEvent(communityId: postViewMedia.postView.community.id)); + break; + default: + break; + } + }, ); }, onEdit: () async { diff --git a/lib/post/widgets/user_post_action_bottom_sheet.dart b/lib/post/widgets/user_post_action_bottom_sheet.dart index 9082071d1..e9254cbe8 100644 --- a/lib/post/widgets/user_post_action_bottom_sheet.dart +++ b/lib/post/widgets/user_post_action_bottom_sheet.dart @@ -71,13 +71,15 @@ class UserPostActionBottomSheet extends StatefulWidget { final PostViewMedia postViewMedia; /// Called when an action is selected - final Function(PersonView? personView) onAction; + final Function(UserAction userAction, PersonView? personView) onAction; @override State createState() => _UserPostActionBottomSheetState(); } class _UserPostActionBottomSheetState extends State { + UserAction? _userAction; + void performAction(UserPostAction action) { switch (action) { case UserPostAction.viewProfile: @@ -86,15 +88,19 @@ class _UserPostActionBottomSheetState extends State { break; case UserPostAction.blockUser: context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.block, value: true)); + setState(() => _userAction = UserAction.block); break; case UserPostAction.unblockUser: context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.block, value: false)); + setState(() => _userAction = UserAction.block); break; case UserPostAction.banUserFromCommunity: context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.banFromCommunity, value: true)); + setState(() => _userAction = UserAction.banFromCommunity); break; case UserPostAction.unbanUserFromCommunity: context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.banFromCommunity, value: false)); + setState(() => _userAction = UserAction.banFromCommunity); break; case UserPostAction.addUserAsCommunityModerator: context.read().add(UserActionEvent( @@ -103,6 +109,7 @@ class _UserPostActionBottomSheetState extends State { value: true, metadata: {"communityId": widget.postViewMedia.postView.community.id}, )); + setState(() => _userAction = UserAction.addModerator); break; case UserPostAction.removeUserAsCommunityModerator: context.read().add(UserActionEvent( @@ -111,6 +118,7 @@ class _UserPostActionBottomSheetState extends State { value: false, metadata: {"communityId": widget.postViewMedia.postView.community.id}, )); + setState(() => _userAction = UserAction.addModerator); break; } } @@ -181,7 +189,8 @@ class _UserPostActionBottomSheetState extends State { listener: (context, state) { if (state.status == UserStatus.success) { context.pop(); - widget.onAction(state.personView); + if (_userAction != null) widget.onAction(_userAction!, state.personView); + setState(() => _userAction = null); } }, child: Column( From e3bc05c10fcda06b53e720dc9ed5d950b28f6598 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Wed, 16 Oct 2024 11:02:28 -0700 Subject: [PATCH 8/9] fix: banning user from community, subscribed state for community --- .../community_post_action_bottom_sheet.dart | 3 +- .../widgets/post_action_bottom_sheet.dart | 1 + .../user_post_action_bottom_sheet.dart | 96 ++++++++++++++++++- lib/user/bloc/user_bloc.dart | 7 +- 4 files changed, 100 insertions(+), 7 deletions(-) diff --git a/lib/post/widgets/community_post_action_bottom_sheet.dart b/lib/post/widgets/community_post_action_bottom_sheet.dart index f47100a8b..b574233f6 100644 --- a/lib/post/widgets/community_post_action_bottom_sheet.dart +++ b/lib/post/widgets/community_post_action_bottom_sheet.dart @@ -103,10 +103,9 @@ class _CommunityPostActionBottomSheetState extends State cbv.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty; - final isSubscribedToCommunity = subscribedCommunities.where((cfv) => cfv.community.actorId == widget.postViewMedia.postView.community.actorId).isNotEmpty; + final isSubscribedToCommunity = widget.postViewMedia.postView.subscribed != SubscribedType.notSubscribed; if (!isLoggedIn) { userActions = userActions.where((action) => action.requiresAuthentication == false).toList(); diff --git a/lib/post/widgets/post_action_bottom_sheet.dart b/lib/post/widgets/post_action_bottom_sheet.dart index 26ac1f630..0c8cb449e 100644 --- a/lib/post/widgets/post_action_bottom_sheet.dart +++ b/lib/post/widgets/post_action_bottom_sheet.dart @@ -121,6 +121,7 @@ class _PostActionBottomSheetState extends State { }, ), GeneralPostAction.user => UserPostActionBottomSheet( + context: widget.context, postViewMedia: widget.postViewMedia, onAction: (UserAction userAction, PersonView? updatedPersonView) { widget.onAction?.call(userAction: userAction, postViewMedia: widget.postViewMedia); diff --git a/lib/post/widgets/user_post_action_bottom_sheet.dart b/lib/post/widgets/user_post_action_bottom_sheet.dart index e9254cbe8..1625e85e8 100644 --- a/lib/post/widgets/user_post_action_bottom_sheet.dart +++ b/lib/post/widgets/user_post_action_bottom_sheet.dart @@ -5,12 +5,16 @@ import 'package:go_router/go_router.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/core/enums/user_type.dart'; import 'package:thunder/core/models/post_view_media.dart'; import 'package:thunder/feed/utils/utils.dart'; import 'package:thunder/feed/view/feed_page.dart'; import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/post/utils/comment_action_helpers.dart'; +import 'package:thunder/shared/avatars/user_avatar.dart'; import 'package:thunder/shared/bottom_sheet_action.dart'; +import 'package:thunder/shared/chips/user_chip.dart'; +import 'package:thunder/shared/dialogs.dart'; import 'package:thunder/shared/divider.dart'; import 'package:thunder/thunder/thunder_icons.dart'; import 'package:thunder/user/bloc/user_bloc.dart'; @@ -65,7 +69,10 @@ enum UserPostAction { /// Given a [postViewMedia] and a [onAction] callback, this widget will display a list of actions that can be taken on the user. /// The [onAction] callback will be triggered when an action is performed. This is useful if the parent widget requires an updated [PersonView]. class UserPostActionBottomSheet extends StatefulWidget { - const UserPostActionBottomSheet({super.key, required this.postViewMedia, required this.onAction}); + const UserPostActionBottomSheet({super.key, required this.context, required this.postViewMedia, required this.onAction}); + + /// The outer context + final BuildContext context; /// The post information final PostViewMedia postViewMedia; @@ -95,11 +102,19 @@ class _UserPostActionBottomSheetState extends State { setState(() => _userAction = UserAction.block); break; case UserPostAction.banUserFromCommunity: - context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.banFromCommunity, value: true)); - setState(() => _userAction = UserAction.banFromCommunity); + showBanUserDialog(); break; case UserPostAction.unbanUserFromCommunity: - context.read().add(UserActionEvent(userId: widget.postViewMedia.postView.creator.id, userAction: UserAction.banFromCommunity, value: false)); + context.read().add( + UserActionEvent( + userId: widget.postViewMedia.postView.creator.id, + userAction: UserAction.banFromCommunity, + value: false, + metadata: { + "communityId": widget.postViewMedia.postView.community.id, + }, + ), + ); setState(() => _userAction = UserAction.banFromCommunity); break; case UserPostAction.addUserAsCommunityModerator: @@ -123,6 +138,79 @@ class _UserPostActionBottomSheetState extends State { } } + void showBanUserDialog() { + /// The controller for the message + TextEditingController messageController = TextEditingController(); + + /// Whether or not the user data (posts and comments) should be removed from the community + bool removeData = false; + + showThunderDialog( + context: widget.context, + title: l10n.banFromCommunity, + primaryButtonText: "Ban", + onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { + widget.context.read().add( + UserActionEvent( + userId: widget.postViewMedia.postView.creator.id, + userAction: UserAction.banFromCommunity, + value: true, + metadata: { + "communityId": widget.postViewMedia.postView.community.id, + "reason": messageController.text, + "removeData": removeData, + }, + ), + ); + setState(() => _userAction = UserAction.banFromCommunity); + dialogContext.pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (context) => context.pop(), + contentWidgetBuilder: (_) => StatefulBuilder( + builder: (context, setState) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UserChip( + person: widget.postViewMedia.postView.creator, + personAvatar: UserAvatar(person: widget.postViewMedia.postView.creator), + userGroups: const [UserType.op], + includeInstance: true, + ), + const SizedBox(height: 16.0), + TextFormField( + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: l10n.message(0), + ), + autofocus: true, + controller: messageController, + maxLines: 2, + ), + const SizedBox(height: 16.0), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Remove user data'), + Switch( + value: removeData, + onChanged: (value) { + setState(() => removeData = value); + }, + ), + ], + ) + ], + ); + }, + ), + ); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); diff --git a/lib/user/bloc/user_bloc.dart b/lib/user/bloc/user_bloc.dart index be0dcfcd5..8775b2f95 100644 --- a/lib/user/bloc/user_bloc.dart +++ b/lib/user/bloc/user_bloc.dart @@ -74,6 +74,11 @@ class UserBloc extends Bloc { int? expires = event.metadata?['expires']; bool removeData = event.metadata?['removeData'] ?? false; + if (expires != null) { + // Convert from milliseconds to seconds + expires = expires ~/ 1000; + } + BanFromCommunityResponse banFromCommunityResponse = await banUserFromCommunity(event.userId, event.value, communityId: communityId, reason: reason, expires: expires, removeData: removeData); emit(state.copyWith( @@ -84,7 +89,7 @@ class UserBloc extends Bloc { : l10n.successfullyUnbannedUser(banFromCommunityResponse.personView.person.name), )); } catch (e) { - return emit(state.copyWith(status: UserStatus.failure)); + return emit(state.copyWith(status: UserStatus.failure, message: e.toString())); } break; case UserAction.addModerator: From 74affdc9ea0d4ea485f5bba829166355f6b2f96f Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Wed, 16 Oct 2024 11:49:20 -0700 Subject: [PATCH 9/9] fix: unable to edit posts from bottom sheet action --- .../post_post_action_bottom_sheet.dart | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/lib/post/widgets/post_post_action_bottom_sheet.dart b/lib/post/widgets/post_post_action_bottom_sheet.dart index 702a2e52e..c0da05752 100644 --- a/lib/post/widgets/post_post_action_bottom_sheet.dart +++ b/lib/post/widgets/post_post_action_bottom_sheet.dart @@ -2,15 +2,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:lemmy_api_client/v3.dart'; +import 'package:swipeable_page_route/swipeable_page_route.dart'; +import 'package:thunder/account/bloc/account_bloc.dart'; +import 'package:thunder/account/models/account.dart'; +import 'package:thunder/community/pages/create_post_page.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/core/auth/helpers/fetch_account.dart'; import 'package:thunder/core/models/post_view_media.dart'; +import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/feed/bloc/feed_bloc.dart'; +import 'package:thunder/post/cubit/create_post_cubit.dart'; import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/post/utils/comment_action_helpers.dart'; +import 'package:thunder/post/utils/navigate_create_post.dart'; import 'package:thunder/shared/bottom_sheet_action.dart'; import 'package:thunder/shared/dialogs.dart'; import 'package:thunder/shared/divider.dart'; +import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/thunder/thunder_icons.dart'; /// Defines the actions that can be taken on a user @@ -78,7 +88,7 @@ class PostPostActionBottomSheet extends StatefulWidget { } class _PostPostActionBottomSheetState extends State { - void performAction(PostPostAction action) { + void performAction(PostPostAction action) async { final postViewMedia = widget.postViewMedia; switch (action) { @@ -87,6 +97,53 @@ class _PostPostActionBottomSheetState extends State { return; case PostPostAction.editPost: context.pop(); + + ThunderBloc thunderBloc = context.read(); + AccountBloc accountBloc = context.read(); + CreatePostCubit createPostCubit = CreatePostCubit(); + + final ThunderState thunderState = context.read().state; + final bool reduceAnimations = thunderState.reduceAnimations; + + Navigator.of(widget.context).push( + SwipeablePageRoute( + transitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : null, + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, + builder: (context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: thunderBloc), + BlocProvider.value(value: accountBloc), + BlocProvider.value(value: createPostCubit), + ], + child: CreatePostPage( + communityId: postViewMedia.postView.community.id, + // Create a stub for the community view. + communityView: CommunityView( + community: postViewMedia.postView.community, + subscribed: postViewMedia.postView.subscribed, + blocked: false, + counts: CommunityAggregates( + communityId: postViewMedia.postView.community.id, + subscribers: 0, + posts: 0, + comments: 0, + published: DateTime.now(), + usersActiveDay: 0, + usersActiveWeek: 0, + usersActiveMonth: 0, + usersActiveHalfYear: 0, + ), + ), + postView: postViewMedia.postView, + onPostSuccess: (PostViewMedia pvm, _) {}, + ), + ); + }, + ), + ); + return; case PostPostAction.deletePost: context.pop();