Skip to content

Commit

Permalink
Added initial moderator post actions (lock, pin, remove) (#1104)
Browse files Browse the repository at this point in the history
* added initial support for moderator pin, lock and remove post

* added dynamic labels for post card actions

* updated dependencies and linting

* added arrow to trailing icon for moderator actions

* moderator remove post uses bottom sheet text field rather than dialogs
  • Loading branch information
hjiangsu authored Feb 12, 2024
1 parent f8d76f4 commit b2ddd0e
Show file tree
Hide file tree
Showing 9 changed files with 442 additions and 70 deletions.
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.1.1):
- permission_handler_apple (9.3.0):
- Flutter
- receive_sharing_intent (0.0.1):
- Flutter
Expand Down Expand Up @@ -126,7 +126,7 @@ SPEC CHECKSUMS:
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
Expand Down
118 changes: 109 additions & 9 deletions lib/community/utils/post_card_action_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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';
Expand All @@ -17,7 +18,9 @@ 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/dialogs.dart';
import 'package:thunder/shared/picker_item.dart';
import 'package:thunder/shared/snackbar.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
Expand Down Expand Up @@ -49,26 +52,34 @@ enum PostCardAction {
toggleRead,
share,
delete,
moderatorActions,
moderatorLockPost,
moderatorPinCommunity,
moderatorRemovePost,
}

class ExtendedPostCardActions {
const ExtendedPostCardActions({
required this.postCardAction,
required this.icon,
this.trailingIcon,
required this.label,
this.color,
this.getForegroundColor,
this.getOverrideIcon,
this.getOverrideLabel,
this.shouldShow,
this.shouldEnable,
});

final PostCardAction postCardAction;
final IconData icon;
final IconData? trailingIcon;
final String label;
final Color? color;
final Color? Function(PostView postView)? getForegroundColor;
final IconData? Function(PostView postView)? getOverrideIcon;
final String? Function(PostView postView)? getOverrideLabel;
final bool Function(BuildContext context, PostView commentView)? shouldShow;
final bool Function(bool isUserLoggedIn)? shouldEnable;
}
Expand Down Expand Up @@ -174,20 +185,64 @@ final List<ExtendedPostCardActions> postCardActionItems = [
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: (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: (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: (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: (postView) => postView.post.removed ? l10n.restorePost : l10n.removePost,
)
];

enum PostActionBottomSheetPage {
general,
share,
moderator,
}

void showPostActionBottomModalSheet(
BuildContext context,
PostViewMedia postViewMedia, {
List<PostCardAction>? actionsToInclude,
List<PostCardAction>? multiActionsToInclude,
PostActionBottomSheetPage postActionBottomSheetPage = PostActionBottomSheetPage.general,
}) {
final theme = Theme.of(context);

final bool isUserLoggedIn = context.read<AuthBloc>().state.isLoggedIn;
final bool useAdvancedShareSheet = context.read<ThunderBloc>().state.useAdvancedShareSheet;
final bool isOwnPost = postViewMedia.postView.creator.id == context.read<AuthBloc>().state.account?.userId;
final bool isDeleted = postViewMedia.postView.post.deleted;

final bool isModerator =
context.read<AccountBloc>().state.moderates.any((CommunityModeratorView communityModeratorView) => communityModeratorView.community.id == postViewMedia.postView.community.id);

actionsToInclude ??= [];
List<ExtendedPostCardActions> postCardActionItemsToUse = postCardActionItems.where((extendedAction) => actionsToInclude!.any((action) => extendedAction.postCardAction == action)).toList();
Expand All @@ -202,12 +257,12 @@ void showPostActionBottomModalSheet(
}

// Add the option to delete one's own posts
if (isOwnPost) {
postCardActionItemsToUse.add(ExtendedPostCardActions(
postCardAction: PostCardAction.delete,
icon: isDeleted ? Icons.restore_from_trash_rounded : Icons.delete_rounded,
label: isDeleted ? AppLocalizations.of(context)!.restore : AppLocalizations.of(context)!.delete,
));
if (isOwnPost && postActionBottomSheetPage == PostActionBottomSheetPage.general) {
postCardActionItemsToUse.add(postCardActionItems.firstWhere((ExtendedPostCardActions extendedPostCardActions) => extendedPostCardActions.postCardAction == PostCardAction.delete));
}

if (isModerator && postActionBottomSheetPage == PostActionBottomSheetPage.general) {
postCardActionItemsToUse.add(postCardActionItems.firstWhere((ExtendedPostCardActions extendedPostCardActions) => extendedPostCardActions.postCardAction == PostCardAction.moderatorActions));
}

multiActionsToInclude ??= [];
Expand Down Expand Up @@ -263,8 +318,9 @@ void showPostActionBottomModalSheet(
}

return PickerItem(
label: postCardActionItemsToUse[index].label,
icon: postCardActionItemsToUse[index].icon,
label: postCardActionItemsToUse[index].getOverrideLabel?.call(postViewMedia.postView) ?? postCardActionItemsToUse[index].label,
icon: postCardActionItemsToUse[index].getOverrideIcon?.call(postViewMedia.postView) ?? postCardActionItemsToUse[index].icon,
trailingIcon: postCardActionItemsToUse[index].trailingIcon,
onSelected: (postCardActionItemsToUse[index].shouldEnable?.call(isUserLoggedIn) ?? true)
? () => onSelected(context, postCardActionItemsToUse[index].postCardAction, postViewMedia, useAdvancedShareSheet)
: null,
Expand Down Expand Up @@ -356,6 +412,7 @@ void onSelected(BuildContext context, PostCardAction postCardAction, PostViewMed
: showPostActionBottomModalSheet(
context,
postViewMedia,
postActionBottomSheetPage: PostActionBottomSheetPage.share,
actionsToInclude: [PostCardAction.sharePost, PostCardAction.shareMedia, PostCardAction.shareLink],
);
break;
Expand All @@ -371,5 +428,48 @@ void onSelected(BuildContext context, PostCardAction postCardAction, PostViewMed
case PostCardAction.delete:
context.read<FeedBloc>().add(FeedItemActionedEvent(postAction: PostAction.delete, postId: postViewMedia.postView.post.id, value: !postViewMedia.postView.post.deleted));
break;
case PostCardAction.moderatorActions:
showPostActionBottomModalSheet(
context,
postViewMedia,
postActionBottomSheetPage: PostActionBottomSheetPage.moderator,
actionsToInclude: [PostCardAction.moderatorLockPost, PostCardAction.moderatorPinCommunity, PostCardAction.moderatorRemovePost],
);
break;
case PostCardAction.moderatorLockPost:
context.read<FeedBloc>().add(FeedItemActionedEvent(postAction: PostAction.lock, postId: postViewMedia.postView.post.id, value: !postViewMedia.postView.post.locked));
break;
case PostCardAction.moderatorPinCommunity:
context.read<FeedBloc>().add(FeedItemActionedEvent(postAction: PostAction.pinCommunity, postId: postViewMedia.postView.post.id, value: !postViewMedia.postView.post.featuredCommunity));
break;
case PostCardAction.moderatorRemovePost:
showRemovePostReasonBottomSheet(context, postViewMedia);
break;
}
}

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<FeedBloc>().add(
FeedItemActionedEvent(
postAction: PostAction.remove,
postId: postViewMedia.postView.post.id,
value: {
'remove': !postViewMedia.postView.post.removed,
'reason': message,
},
),
);
Navigator.of(context).pop();
},
),
);
}
18 changes: 18 additions & 0 deletions lib/community/widgets/post_card_view_comfortable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,16 @@ class PostCardViewComfortable extends StatelessWidget {
color: indicateRead && postViewMedia.postView.read ? Colors.red.withOpacity(0.55) : Colors.red,
),
),
if (postViewMedia.postView.post.removed)
WidgetSpan(
child: Icon(
Icons.delete_forever_rounded,
size: 16 * textScaleFactor,
color: indicateRead && postViewMedia.postView.read ? Colors.red.withOpacity(0.55) : Colors.red,
),
),
if (postViewMedia.postView.post.deleted ||
postViewMedia.postView.post.removed ||
postViewMedia.postView.post.featuredCommunity ||
postViewMedia.postView.post.featuredLocal ||
(!useSaveButton && postViewMedia.postView.saved) ||
Expand Down Expand Up @@ -216,7 +225,16 @@ class PostCardViewComfortable extends StatelessWidget {
color: indicateRead && postViewMedia.postView.read ? Colors.red.withOpacity(0.55) : Colors.red,
),
),
if (postViewMedia.postView.post.removed)
WidgetSpan(
child: Icon(
Icons.delete_forever_rounded,
size: 16 * textScaleFactor,
color: indicateRead && postViewMedia.postView.read ? Colors.red.withOpacity(0.55) : Colors.red,
),
),
if (postViewMedia.postView.post.deleted ||
postViewMedia.postView.post.removed ||
postViewMedia.postView.post.featuredCommunity ||
postViewMedia.postView.post.featuredLocal ||
(!useSaveButton && postViewMedia.postView.saved) ||
Expand Down
9 changes: 9 additions & 0 deletions lib/community/widgets/post_card_view_compact.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,16 @@ class PostCardViewCompact extends StatelessWidget {
color: indicateRead && postViewMedia.postView.read ? Colors.red.withOpacity(0.55) : Colors.red,
),
),
if (postViewMedia.postView.post.removed)
WidgetSpan(
child: Icon(
Icons.delete_forever_rounded,
size: 16 * textScaleFactor,
color: indicateRead && postViewMedia.postView.read ? Colors.red.withOpacity(0.55) : Colors.red,
),
),
if (postViewMedia.postView.post.deleted ||
postViewMedia.postView.post.removed ||
postViewMedia.postView.post.featuredCommunity ||
postViewMedia.postView.post.featuredLocal ||
postViewMedia.postView.saved ||
Expand Down
78 changes: 75 additions & 3 deletions lib/feed/bloc/feed_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,83 @@ class FeedBloc extends Bloc<FeedEvent, FeedState> {
case PostAction.report:
// TODO: Handle this case.
case PostAction.lock:
// TODO: Handle this case.
// Optimistically lock the post
int existingPostViewMediaIndex = state.postViewMedias.indexWhere((PostViewMedia postViewMedia) => postViewMedia.postView.post.id == event.postId);

PostViewMedia postViewMedia = state.postViewMedias[existingPostViewMediaIndex];
PostView originalPostView = postViewMedia.postView;

try {
PostView updatedPostView = optimisticallyLockPost(postViewMedia.postView, event.value);
state.postViewMedias[existingPostViewMediaIndex].postView = updatedPostView;

// Emit the state to update UI immediately
emit(state.copyWith(status: FeedStatus.success));
emit(state.copyWith(status: FeedStatus.fetching));

bool success = await lockPost(originalPostView.post.id, event.value);
if (success) return emit(state.copyWith(status: FeedStatus.success));

// Restore the original post contents if not successful
state.postViewMedias[existingPostViewMediaIndex].postView = originalPostView;
return emit(state.copyWith(status: FeedStatus.failure));
} catch (e) {
// Restore the original post contents
state.postViewMedias[existingPostViewMediaIndex].postView = originalPostView;
return emit(state.copyWith(status: FeedStatus.failure));
}
case PostAction.pinCommunity:
// TODO: Handle this case.
// Optimistically pin the post to the community
int existingPostViewMediaIndex = state.postViewMedias.indexWhere((PostViewMedia postViewMedia) => postViewMedia.postView.post.id == event.postId);

PostViewMedia postViewMedia = state.postViewMedias[existingPostViewMediaIndex];
PostView originalPostView = postViewMedia.postView;

try {
PostView updatedPostView = optimisticallyPinPostToCommunity(postViewMedia.postView, event.value);
state.postViewMedias[existingPostViewMediaIndex].postView = updatedPostView;

// Emit the state to update UI immediately
emit(state.copyWith(status: FeedStatus.success));
emit(state.copyWith(status: FeedStatus.fetching));

bool success = await pinPostToCommunity(originalPostView.post.id, event.value);
if (success) return emit(state.copyWith(status: FeedStatus.success));

// Restore the original post contents if not successful
state.postViewMedias[existingPostViewMediaIndex].postView = originalPostView;
return emit(state.copyWith(status: FeedStatus.failure));
} catch (e) {
// Restore the original post contents
state.postViewMedias[existingPostViewMediaIndex].postView = originalPostView;
return emit(state.copyWith(status: FeedStatus.failure));
}
case PostAction.remove:
// TODO: Handle this case.
// Optimistically remove the post from the community
int existingPostViewMediaIndex = state.postViewMedias.indexWhere((PostViewMedia postViewMedia) => postViewMedia.postView.post.id == event.postId);

PostViewMedia postViewMedia = state.postViewMedias[existingPostViewMediaIndex];
PostView originalPostView = postViewMedia.postView;

try {
PostView updatedPostView = optimisticallyRemovePost(postViewMedia.postView, event.value['remove']);
state.postViewMedias[existingPostViewMediaIndex].postView = updatedPostView;

// Emit the state to update UI immediately
emit(state.copyWith(status: FeedStatus.success));
emit(state.copyWith(status: FeedStatus.fetching));

bool success = await removePost(originalPostView.post.id, event.value['remove'], event.value['reason']);
if (success) return emit(state.copyWith(status: FeedStatus.success));

// Restore the original post contents if not successful
state.postViewMedias[existingPostViewMediaIndex].postView = originalPostView;
return emit(state.copyWith(status: FeedStatus.failure));
} catch (e) {
// Restore the original post contents
state.postViewMedias[existingPostViewMediaIndex].postView = originalPostView;
return emit(state.copyWith(status: FeedStatus.failure));
}
case PostAction.pinInstance:
// TODO: Handle this case.
case PostAction.purge:
Expand Down
Loading

0 comments on commit b2ddd0e

Please sign in to comment.