From 5628b0c28a61a17ecbe11330e07d2625e8514e3c Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Tue, 12 Mar 2024 15:28:27 -0400 Subject: [PATCH 1/2] Improve post/comment bottom sheets --- .../utils/post_card_action_helpers.dart | 612 ++++++++++++------ lib/community/widgets/post_card.dart | 16 - .../widgets/post_card_view_comfortable.dart | 16 - lib/l10n/app_en.arb | 24 + lib/post/pages/create_comment_page.dart | 15 +- lib/post/utils/comment_action_helpers.dart | 458 +++++++++---- lib/post/widgets/post_view.dart | 19 +- lib/shared/picker_item.dart | 2 + 8 files changed, 780 insertions(+), 382 deletions(-) diff --git a/lib/community/utils/post_card_action_helpers.dart b/lib/community/utils/post_card_action_helpers.dart index 0ea788108..93b469b38 100644 --- a/lib/community/utils/post_card_action_helpers.dart +++ b/lib/community/utils/post_card_action_helpers.dart @@ -1,4 +1,6 @@ +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'; @@ -9,6 +11,7 @@ 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/core/enums/full_name_separator.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'; @@ -20,7 +23,6 @@ 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'; @@ -35,12 +37,15 @@ 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, visitInstance, blockInstance, sharePost, @@ -79,7 +84,7 @@ class ExtendedPostCardActions { final Color? color; final Color? Function(PostView postView)? getForegroundColor; final IconData? Function(PostView postView)? getOverrideIcon; - final String? Function(PostView postView)? getOverrideLabel; + final String? Function(BuildContext context, PostView postView)? getOverrideLabel; final bool Function(BuildContext context, PostView commentView)? shouldShow; final bool Function(bool isUserLoggedIn)? shouldEnable; } @@ -87,6 +92,13 @@ class ExtendedPostCardActions { final l10n = AppLocalizations.of(GlobalContext.context)!; final List postCardActionItems = [ + ExtendedPostCardActions( + postCardAction: PostCardAction.userActions, + icon: Icons.person_rounded, + label: '', + getOverrideLabel: (context, postView) => l10n.userEntry(generateUserFullName(context, postView.creator.name, fetchInstanceNameFromUrl(postView.creator.actorId))), + trailingIcon: Icons.chevron_right_rounded, + ), ExtendedPostCardActions( postCardAction: PostCardAction.visitProfile, icon: Icons.person_search_rounded, @@ -98,6 +110,13 @@ final List postCardActionItems = [ label: l10n.blockUser, shouldEnable: (isUserLoggedIn) => isUserLoggedIn, ), + ExtendedPostCardActions( + postCardAction: PostCardAction.communityActions, + icon: Icons.people_rounded, + label: '', + getOverrideLabel: (context, postView) => l10n.communityEntry(generateCommunityFullName(context, postView.community.name, fetchInstanceNameFromUrl(postView.community.actorId))), + trailingIcon: Icons.chevron_right_rounded, + ), ExtendedPostCardActions( postCardAction: PostCardAction.visitCommunity, icon: Icons.home_work_rounded, @@ -121,6 +140,13 @@ final List postCardActionItems = [ label: l10n.blockCommunity, shouldEnable: (isUserLoggedIn) => isUserLoggedIn, ), + ExtendedPostCardActions( + postCardAction: PostCardAction.instanceActions, + icon: Icons.language_rounded, + label: '', + getOverrideLabel: (context, postView) => l10n.instanceEntry(fetchInstanceNameFromUrl(postView.community.actorId) ?? ''), + trailingIcon: Icons.chevron_right_rounded, + ), ExtendedPostCardActions( postCardAction: PostCardAction.visitInstance, icon: Icons.language, @@ -191,7 +217,7 @@ final List postCardActionItems = [ 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, + getOverrideLabel: (context, postView) => postView.post.deleted ? l10n.restore : l10n.delete, ), ExtendedPostCardActions( postCardAction: PostCardAction.moderatorActions, @@ -204,21 +230,21 @@ final List postCardActionItems = [ 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, + 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: (postView) => postView.post.featuredCommunity ? l10n.unpinFromCommunity : l10n.pinToCommunity, + 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: (postView) => postView.post.removed ? l10n.restorePost : l10n.removePost, + getOverrideLabel: (context, postView) => postView.post.removed ? l10n.restorePost : l10n.removePost, ) ]; @@ -226,226 +252,432 @@ enum PostActionBottomSheetPage { general, share, moderator, + user, + community, + instance, } void showPostActionBottomModalSheet( BuildContext context, PostViewMedia postViewMedia, { - List? actionsToInclude, - List? multiActionsToInclude, - PostActionBottomSheetPage postActionBottomSheetPage = PostActionBottomSheetPage.general, + PostActionBottomSheetPage page = PostActionBottomSheetPage.general, // todo: is this needed? }) { - final theme = Theme.of(context); - - final bool isUserLoggedIn = context.read().state.isLoggedIn; - final bool useAdvancedShareSheet = context.read().state.useAdvancedShareSheet; 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); - actionsToInclude ??= []; - List postCardActionItemsToUse = postCardActionItems.where((extendedAction) => actionsToInclude!.any((action) => extendedAction.postCardAction == action)).toList(); + // 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(); - if (actionsToInclude.contains(PostCardAction.blockInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { - postCardActionItemsToUse.removeWhere((ExtendedPostCardActions postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockInstance); + // Add the moderator actions submenu + if (isModerator) { + defaultPostCardActions.add(postCardActionItems.firstWhere((ExtendedPostCardActions extendedPostCardActions) => extendedPostCardActions.postCardAction == PostCardAction.moderatorActions)); } - // Hide the option to block a community if the user is subscribed to it - if (actionsToInclude.contains(PostCardAction.blockCommunity) && postViewMedia.postView.subscribed != SubscribedType.notSubscribed) { - postCardActionItemsToUse.removeWhere((ExtendedPostCardActions postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockCommunity); + // Generate the list of default multi actions + final List defaultMultiPostCardActions = postCardActionItems + .where((extendedAction) => [ + PostCardAction.upvote, + PostCardAction.downvote, + PostCardAction.save, + PostCardAction.toggleRead, + PostCardAction.share, + if (isOwnPost) PostCardAction.delete, + ].contains(extendedAction.postCardAction)) + .toList(); + + // 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.shareMedia, + PostCardAction.shareLink, + ].contains(extendedAction.postCardAction)) + .toList(); + + // Remove the share link option if there is no link + if (postViewMedia.media.isEmpty || (postViewMedia.media.first.mediaType != MediaType.link && postViewMedia.media.first.mediaType != MediaType.image)) { + sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareLink); } - // Add the option to delete one's own posts - if (isOwnPost && postActionBottomSheetPage == PostActionBottomSheetPage.general) { - postCardActionItemsToUse.add(postCardActionItems.firstWhere((ExtendedPostCardActions extendedPostCardActions) => extendedPostCardActions.postCardAction == PostCardAction.delete)); + // Remove the share media option if there is no media + if (postViewMedia.media.isEmpty || postViewMedia.media.first.mediaUrl == null) { + sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareMedia); } - if (isModerator && postActionBottomSheetPage == PostActionBottomSheetPage.general) { - postCardActionItemsToUse.add(postCardActionItems.firstWhere((ExtendedPostCardActions extendedPostCardActions) => extendedPostCardActions.postCardAction == PostCardAction.moderatorActions)); + // Generate the list of user actions + final List userActions = postCardActionItems + .where((extendedAction) => [ + PostCardAction.visitProfile, + 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); } - multiActionsToInclude ??= []; - final multiPostCardActionItemsToUse = postCardActionItems.where((extendedAction) => multiActionsToInclude!.any((action) => extendedAction.postCardAction == action)).toList(); + // Generate the list of instance actions + final List instanceActions = postCardActionItems + .where((extendedAction) => [ + PostCardAction.visitInstance, + PostCardAction.blockInstance, + ].contains(extendedAction.postCardAction)) + .toList(); + +// Remove block if unsupported + if (instanceActions.any((extendedAction) => extendedAction.postCardAction == PostCardAction.blockInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { + instanceActions.removeWhere((ExtendedPostCardActions postCardActionItem) => postCardActionItem.postCardAction == PostCardAction.blockInstance); + } showModalBottomSheet( showDragHandle: true, isScrollControlled: true, context: context, - builder: (BuildContext bottomSheetContext) { - return SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0, left: 26.0, right: 16.0), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - AppLocalizations.of(context)!.actions, - style: theme.textTheme.titleLarge!.copyWith(), + 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, + ), + ); +} + +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; + + const PostCardActionPicker({ + super.key, + required this.postViewMedia, + required this.page, + required this.postCardActions, + required this.multiPostCardActions, + required this.titles, + required this.outerContext, + }); + + @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, + ), + ), + ], + ), + ), + ), + ), + ), ), ), - ), - MultiPickerItem( - pickerItems: [ - ...multiPostCardActionItemsToUse.where((a) => a.shouldShow?.call(context, postViewMedia.postView) ?? true).map( - (a) { - return PickerItemData( - label: a.label, - icon: a.getOverrideIcon?.call(postViewMedia.postView) ?? a.icon, - backgroundColor: a.color, - foregroundColor: a.getForegroundColor?.call(postViewMedia.postView), - onSelected: (a.shouldEnable?.call(isUserLoggedIn) ?? true) ? () => onSelected(context, a.postCardAction, postViewMedia, useAdvancedShareSheet) : null, + 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.label, + icon: a.getOverrideIcon?.call(widget.postViewMedia.postView) ?? a.icon, + backgroundColor: a.color, + foregroundColor: a.getForegroundColor?.call(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, + 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, ); }, ), - ], - ), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: postCardActionItemsToUse.length, - itemBuilder: (BuildContext itemBuilderContext, int index) { - if (postCardActionItemsToUse[index].postCardAction == PostCardAction.shareLink && - (postViewMedia.media.isEmpty || (postViewMedia.media.first.mediaType != MediaType.link && postViewMedia.media.first.mediaType != MediaType.image))) { - return Container(); - } - - if (postCardActionItemsToUse[index].postCardAction == PostCardAction.shareMedia && (postViewMedia.media.isEmpty || postViewMedia.media.first.mediaUrl == null)) { - return Container(); - } - - return PickerItem( - 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, - ); - }, - ), - const SizedBox(height: 16.0), - ], + const SizedBox(height: 16.0), + ], + ), ), - ); - }, - ); -} + ), + ); + } -void onTapCommunityName(BuildContext context, int communityId) { - navigateToFeedPage(context, feedType: FeedType.community, communityId: communityId); -} + void onSelected(PostCardAction postCardAction) async { + bool pop = true; + void Function() action; -void onSelected(BuildContext context, PostCardAction postCardAction, PostViewMedia postViewMedia, bool useAdvancedShareSheet) async { - Navigator.of(context).pop(); - - switch (postCardAction) { - case PostCardAction.visitCommunity: - onTapCommunityName(context, postViewMedia.postView.community.id); - break; - case PostCardAction.visitProfile: - navigateToFeedPage(context, feedType: FeedType.user, userId: postViewMedia.postView.post.creatorId); - break; - case PostCardAction.visitInstance: - navigateToInstancePage(context, instanceHost: fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId)!, instanceId: postViewMedia.postView.community.instanceId); - break; - case PostCardAction.sharePost: - Share.share(postViewMedia.postView.post.apId); - break; - case PostCardAction.shareMedia: - if (postViewMedia.media.first.mediaUrl != null) { - try { - // Try to get the cached image first - var media = await DefaultCacheManager().getFileFromCache(postViewMedia.media.first.mediaUrl!); - File? mediaFile = media?.file; - - if (media == null) { - // Tell user we're downloading the image - showSnackbar(AppLocalizations.of(context)!.downloadingMedia); - - // Download - mediaFile = await DefaultCacheManager().getSingleFile(postViewMedia.media.first.mediaUrl!); - } + 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.visitInstance: + action = () => navigateToInstancePage(widget.outerContext, + instanceHost: fetchInstanceNameFromUrl(widget.postViewMedia.postView.community.actorId)!, instanceId: widget.postViewMedia.postView.community.instanceId); + break; + case PostCardAction.sharePost: + action = () => Share.share(widget.postViewMedia.postView.post.apId); + break; + case PostCardAction.shareMedia: + action = () async { + if (widget.postViewMedia.media.first.mediaUrl != null) { + try { + // Try to get the cached image first + var media = await DefaultCacheManager().getFileFromCache(widget.postViewMedia.media.first.mediaUrl!); + File? mediaFile = media?.file; + + if (media == null) { + // Tell user we're downloading the image + showSnackbar(AppLocalizations.of(widget.outerContext)!.downloadingMedia); - // Share - await Share.shareXFiles([XFile(mediaFile!.path)]); - } catch (e) { - // Tell the user that the download failed - showSnackbar(AppLocalizations.of(context)!.errorDownloadingMedia(e)); + // Download + mediaFile = await DefaultCacheManager().getSingleFile(widget.postViewMedia.media.first.mediaUrl!); + } + + // 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.shareLink: + action = () { + if (widget.postViewMedia.media.first.originalUrl != null) Share.share(widget.postViewMedia.media.first.originalUrl!); + }; + break; + case PostCardAction.instanceActions: + action = () => setState(() => page = PostActionBottomSheetPage.instance); + pop = false; + break; + case PostCardAction.blockInstance: + 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.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)); + 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.share: + final bool useAdvancedShareSheet = widget.outerContext.read().state.useAdvancedShareSheet; + if (useAdvancedShareSheet) { + action = () => showAdvancedShareSheet(widget.outerContext, widget.postViewMedia); + } else { + pop = false; + action = () => setState(() => page = PostActionBottomSheetPage.share); } - } - break; - case PostCardAction.shareLink: - if (postViewMedia.media.first.originalUrl != null) Share.share(postViewMedia.media.first.originalUrl!); - break; - case PostCardAction.blockInstance: - context.read().add(InstanceActionEvent( - instanceAction: InstanceAction.block, - instanceId: postViewMedia.postView.community.instanceId, - domain: fetchInstanceNameFromUrl(postViewMedia.postView.community.actorId), - value: true, - )); - break; - case PostCardAction.blockCommunity: - context.read().add(CommunityActionEvent(communityAction: CommunityAction.block, communityId: postViewMedia.postView.community.id, value: true)); - break; - case PostCardAction.upvote: - context.read().add(FeedItemActionedEvent(postAction: PostAction.vote, postId: postViewMedia.postView.post.id, value: postViewMedia.postView.myVote == 1 ? 0 : 1)); - break; - case PostCardAction.downvote: - context.read().add(FeedItemActionedEvent(postAction: PostAction.vote, postId: postViewMedia.postView.post.id, value: postViewMedia.postView.myVote == -1 ? 0 : -1)); - break; - case PostCardAction.save: - context.read().add(FeedItemActionedEvent(postAction: PostAction.save, postId: postViewMedia.postView.post.id, value: !postViewMedia.postView.saved)); - break; - case PostCardAction.toggleRead: - context.read().add(FeedItemActionedEvent(postAction: PostAction.read, postId: postViewMedia.postView.post.id, value: !postViewMedia.postView.read)); - break; - case PostCardAction.share: - useAdvancedShareSheet - ? showAdvancedShareSheet(context, postViewMedia) - : postViewMedia.media.isEmpty - ? Share.share(postViewMedia.postView.post.apId) - : showPostActionBottomModalSheet( - context, - postViewMedia, - postActionBottomSheetPage: PostActionBottomSheetPage.share, - actionsToInclude: [PostCardAction.sharePost, PostCardAction.shareMedia, PostCardAction.shareLink], - ); - break; - case PostCardAction.blockUser: - context.read().add(UserActionEvent(userAction: UserAction.block, userId: postViewMedia.postView.creator.id, value: true)); - break; - case PostCardAction.subscribeToCommunity: - context.read().add(CommunityActionEvent(communityAction: CommunityAction.follow, communityId: postViewMedia.postView.community.id, value: true)); - break; - case PostCardAction.unsubscribeFromCommunity: - context.read().add(CommunityActionEvent(communityAction: CommunityAction.follow, communityId: postViewMedia.postView.community.id, value: false)); - break; - case PostCardAction.delete: - context.read().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().add(FeedItemActionedEvent(postAction: PostAction.lock, postId: postViewMedia.postView.post.id, value: !postViewMedia.postView.post.locked)); - break; - case PostCardAction.moderatorPinCommunity: - context.read().add(FeedItemActionedEvent(postAction: PostAction.pinCommunity, postId: postViewMedia.postView.post.id, value: !postViewMedia.postView.post.featuredCommunity)); - break; - case PostCardAction.moderatorRemovePost: - showRemovePostReasonBottomSheet(context, postViewMedia); - break; + break; + case PostCardAction.blockUser: + action = () => widget.outerContext.read().add(UserActionEvent(userAction: UserAction.block, userId: widget.postViewMedia.postView.creator.id, value: true)); + 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) { diff --git a/lib/community/widgets/post_card.dart b/lib/community/widgets/post_card.dart index fd60f4e70..43e64719d 100644 --- a/lib/community/widgets/post_card.dart +++ b/lib/community/widgets/post_card.dart @@ -236,22 +236,6 @@ class _PostCardState extends State { onLongPress: () => showPostActionBottomModalSheet( context, widget.postViewMedia, - actionsToInclude: [ - PostCardAction.visitInstance, - PostCardAction.visitProfile, - PostCardAction.blockUser, - PostCardAction.blockInstance, - PostCardAction.visitCommunity, - widget.postViewMedia.postView.subscribed == SubscribedType.notSubscribed ? PostCardAction.subscribeToCommunity : PostCardAction.unsubscribeFromCommunity, - PostCardAction.blockCommunity, - ], - multiActionsToInclude: [ - PostCardAction.upvote, - PostCardAction.downvote, - PostCardAction.save, - PostCardAction.toggleRead, - PostCardAction.share, - ], ), 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 c464676e3..11da3c558 100644 --- a/lib/community/widgets/post_card_view_comfortable.dart +++ b/lib/community/widgets/post_card_view_comfortable.dart @@ -318,22 +318,6 @@ class PostCardViewComfortable extends StatelessWidget { showPostActionBottomModalSheet( context, postViewMedia, - actionsToInclude: [ - PostCardAction.visitInstance, - PostCardAction.visitProfile, - PostCardAction.blockUser, - PostCardAction.blockInstance, - PostCardAction.visitCommunity, - postViewMedia.postView.subscribed == SubscribedType.notSubscribed ? PostCardAction.subscribeToCommunity : PostCardAction.unsubscribeFromCommunity, - PostCardAction.blockCommunity, - ], - multiActionsToInclude: [ - PostCardAction.upvote, - PostCardAction.downvote, - PostCardAction.save, - PostCardAction.toggleRead, - PostCardAction.share, - ], ); HapticFeedback.mediumImpact(); }), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1f41fa454..42d9a2596 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -247,6 +247,14 @@ "@communities": {}, "community": "Community", "@community": {}, + "communityActions": "Community Actions", + "@communityActions": { + "description": "Heading for community actions page" + }, + "communityEntry": "Community '{community}'", + "@communityEntry": { + "description": "Entry in list for a community to perform further actions" + }, "communityFormat": "Community Format", "@communityFormat": { "description": "Setting for community full name format" @@ -655,6 +663,14 @@ "@instance": { "description": "Heading for an instance" }, + "instanceActions": "Instance Actions", + "@instanceActions": { + "description": "Heading for instance actions page" + }, + "instanceEntry": "Instance '{username}'", + "@instanceEntry": { + "description": "Entry in list for a instance to perform further actions" + }, "instanceHasAlreadyBenAdded": "{instance} has already been added.", "@instanceHasAlreadyBenAdded": {}, "internetOrInstanceIssues": "You may not be connected to the Internet, or your instance may be currently unavailable.", @@ -1669,6 +1685,14 @@ "@user": { "description": "Role name for user" }, + "userActions": "User Actions", + "@userActions": { + "description": "Heading for user actions page" + }, + "userEntry": "User '{username}'", + "@userEntry": { + "description": "Entry in list for a user to perform further actions" + }, "userFormat": "User Format", "@userFormat": { "description": "Setting for user full name format" diff --git a/lib/post/pages/create_comment_page.dart b/lib/post/pages/create_comment_page.dart index 53623ca90..388aedfdf 100644 --- a/lib/post/pages/create_comment_page.dart +++ b/lib/post/pages/create_comment_page.dart @@ -279,13 +279,14 @@ class _CreateCommentPageState extends State { style: theme.textTheme.titleMedium, ), ), - MediaView( - scrapeMissingPreviews: thunderState.scrapeMissingPreviews, - postViewMedia: widget.postView!, - hideNsfwPreviews: thunderState.hideNsfwPreviews, - markPostReadOnMediaView: thunderState.markPostReadOnMediaView, - isUserLoggedIn: true, - ), + if (widget.postView != null) + MediaView( + scrapeMissingPreviews: thunderState.scrapeMissingPreviews, + postViewMedia: widget.postView!, + hideNsfwPreviews: thunderState.hideNsfwPreviews, + markPostReadOnMediaView: thunderState.markPostReadOnMediaView, + isUserLoggedIn: true, + ), const SizedBox( height: 12, ), diff --git a/lib/post/utils/comment_action_helpers.dart b/lib/post/utils/comment_action_helpers.dart index 410fb69c1..0e7b9db68 100644 --- a/lib/post/utils/comment_action_helpers.dart +++ b/lib/post/utils/comment_action_helpers.dart @@ -1,8 +1,12 @@ +import 'dart:async'; + +import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:thunder/core/enums/full_name_separator.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/feed/utils/utils.dart'; import 'package:thunder/feed/view/feed_page.dart'; @@ -32,8 +36,10 @@ enum CommentCardAction { reply, edit, report, + userActions, visitProfile, blockUser, + instanceActions, visitInstance, blockInstance, } @@ -42,25 +48,38 @@ class ExtendedCommentCardActions { const ExtendedCommentCardActions({ required this.commentCardAction, required this.icon, + this.trailingIcon, required this.label, this.color, this.getForegroundColor, this.getOverrideIcon, + this.getOverrideLabel, this.shouldShow, this.shouldEnable, }); final CommentCardAction commentCardAction; final IconData icon; + final IconData? trailingIcon; final String label; final Color? color; final Color? Function(CommentView commentView)? getForegroundColor; final IconData? Function(CommentView commentView)? getOverrideIcon; + final String Function(BuildContext context, CommentView commentView)? getOverrideLabel; final bool Function(BuildContext context, CommentView commentView)? shouldShow; final bool Function(bool isUserLoggedIn)? shouldEnable; } +final l10n = AppLocalizations.of(GlobalContext.context)!; + final List commentCardDefaultActionItems = [ + ExtendedCommentCardActions( + commentCardAction: CommentCardAction.userActions, + icon: Icons.person_rounded, + label: '', + getOverrideLabel: (context, commentView) => l10n.userEntry(generateUserFullName(context, commentView.creator.name, fetchInstanceNameFromUrl(commentView.creator.actorId))), + trailingIcon: Icons.chevron_right_rounded, + ), ExtendedCommentCardActions( commentCardAction: CommentCardAction.visitProfile, icon: Icons.person_search_rounded, @@ -72,6 +91,13 @@ final List commentCardDefaultActionItems = [ label: AppLocalizations.of(GlobalContext.context)!.blockUser, shouldEnable: (isUserLoggedIn) => isUserLoggedIn, ), + ExtendedCommentCardActions( + commentCardAction: CommentCardAction.instanceActions, + icon: Icons.language_rounded, + label: '', + getOverrideLabel: (context, postView) => l10n.instanceEntry(fetchInstanceNameFromUrl(postView.creator.actorId) ?? ''), + trailingIcon: Icons.chevron_right_rounded, + ), ExtendedCommentCardActions( commentCardAction: CommentCardAction.visitInstance, icon: Icons.language, @@ -143,167 +169,315 @@ final List commentCardDefaultMultiActionItems = [ ), ]; +enum CommentActionBottomSheetPage { + general, + user, + instance, +} + void showCommentActionBottomModalSheet( - BuildContext context, CommentView commentView, Function onSaveAction, Function onDeleteAction, Function onVoteAction, Function onReplyEditAction, Function onReportAction) { - final theme = Theme.of(context); - final bool isUserLoggedIn = context.read().state.isLoggedIn; - List commentCardActionItems = _updateDefaultCommentActionItems(context, commentView); + BuildContext context, + CommentView commentView, + Function onSaveAction, + Function onDeleteAction, + Function onVoteAction, + Function onReplyEditAction, + Function onReportAction, +) { + final bool isOwnComment = commentView.creator.id == context.read().state.account?.userId; + bool isDeleted = commentView.comment.deleted; + + // Generate the list of default actions for the general page + final List defaultCommentCardActions = commentCardDefaultActionItems + .where((extendedAction) => [ + CommentCardAction.copyText, + CommentCardAction.delete, + CommentCardAction.report, + CommentCardAction.userActions, + CommentCardAction.instanceActions, + ].contains(extendedAction.commentCardAction)) + .toList(); + + // Add the ability to delete one's own comment + if (isOwnComment) { + defaultCommentCardActions.add(ExtendedCommentCardActions( + commentCardAction: CommentCardAction.delete, + icon: isDeleted ? Icons.restore_from_trash_rounded : Icons.delete_rounded, + label: isDeleted ? AppLocalizations.of(GlobalContext.context)!.restore : AppLocalizations.of(GlobalContext.context)!.delete, + )); + } - if (commentCardActionItems.any((c) => c.commentCardAction == CommentCardAction.blockInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { - commentCardActionItems.removeWhere((c) => c.commentCardAction == CommentCardAction.blockInstance); + // Hide the ability to block instance if not supported -- todo change this to instance list + if (defaultCommentCardActions.any((c) => c.commentCardAction == CommentCardAction.blockInstance) && !LemmyClient.instance.supportsFeature(LemmyFeature.blockInstance)) { + defaultCommentCardActions.removeWhere((c) => c.commentCardAction == CommentCardAction.blockInstance); } + // Generate list of user actions + final List userActions = commentCardDefaultActionItems + .where((extendedAction) => [ + CommentCardAction.visitProfile, + CommentCardAction.blockUser, + ].contains(extendedAction.commentCardAction)) + .toList(); + + // Generate list of instance actions + final List instanceActions = commentCardDefaultActionItems + .where((extendedAction) => [ + CommentCardAction.visitInstance, + CommentCardAction.blockInstance, + ].contains(extendedAction.commentCardAction)) + .toList(); + showModalBottomSheet( showDragHandle: true, isScrollControlled: true, context: context, - builder: (BuildContext bottomSheetContext) { - return SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0, left: 26.0, right: 16.0), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - AppLocalizations.of(context)!.actions, - style: theme.textTheme.titleLarge!.copyWith(), + builder: (builderContext) => CommentActionPicker( + outerContext: context, + commentView: commentView, + titles: { + CommentActionBottomSheetPage.general: l10n.actions, + CommentActionBottomSheetPage.user: l10n.userActions, + CommentActionBottomSheetPage.instance: l10n.instanceActions, + }, + multiCommentCardActions: {CommentActionBottomSheetPage.general: commentCardDefaultMultiActionItems}, + commentCardActions: { + CommentActionBottomSheetPage.general: defaultCommentCardActions, + CommentActionBottomSheetPage.user: userActions, + CommentActionBottomSheetPage.instance: instanceActions, + }, + onSaveAction: onSaveAction, + onDeleteAction: onDeleteAction, + onVoteAction: onVoteAction, + onReplyEditAction: onReplyEditAction, + onReportAction: onReportAction, + ), + ); +} + +class CommentActionPicker extends StatefulWidget { + /// The comment + final CommentView commentView; + + /// This is the set of titles to show for each page + final Map titles; + + /// This is the list of quick actions that are shown horizontally across the top of the sheet + final Map> multiCommentCardActions; + + /// This is the set of full actions to display vertically in a list + final Map> commentCardActions; + + /// The context from whoever invoked this sheet (useful for blocs that would otherwise be missing) + final BuildContext outerContext; + + // Callback functions + final Function onSaveAction; + final Function onDeleteAction; + final Function onVoteAction; + final Function onReplyEditAction; + final Function onReportAction; + + const CommentActionPicker({ + super.key, + required this.outerContext, + required this.commentView, + required this.titles, + required this.multiCommentCardActions, + required this.commentCardActions, + required this.onSaveAction, + required this.onDeleteAction, + required this.onVoteAction, + required this.onReplyEditAction, + required this.onReportAction, + }); + + @override + State createState() => _CommentActionPickerState(); +} + +class _CommentActionPickerState extends State { + /// The current page + CommentActionBottomSheetPage page = CommentActionBottomSheetPage.general; + + @override + void initState() { + super.initState(); + + BackButtonInterceptor.add(_handleBack); + } + + @override + void dispose() { + BackButtonInterceptor.remove(_handleBack); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final AppLocalizations l10n = AppLocalizations.of(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] ?? l10n.actions}, ${page == CommentActionBottomSheetPage.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 == CommentActionBottomSheetPage.general ? null : () => setState(() => page = CommentActionBottomSheetPage.general), + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 10, 16.0, 10.0), + child: Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + if (page != CommentActionBottomSheetPage.general) ...[ + const Icon(Icons.chevron_left, size: 30), + const SizedBox(width: 12), + ], + Semantics( + excludeSemantics: true, + child: Text( + widget.titles[page] ?? l10n.actions, + style: theme.textTheme.titleLarge, + ), + ), + ], + ), + ), + ), + ), + ), ), ), - ), - MultiPickerItem( - pickerItems: [ - ...commentCardDefaultMultiActionItems.where((a) => a.shouldShow?.call(context, commentView) ?? true).map( - (a) { - return PickerItemData( - label: a.label, - icon: a.getOverrideIcon?.call(commentView) ?? a.icon, - backgroundColor: a.color, - foregroundColor: a.getForegroundColor?.call(commentView), - onSelected: (a.shouldEnable?.call(isUserLoggedIn) ?? true) - ? () => onSelected( - context, - a.commentCardAction, - commentView, - onSaveAction, - onDeleteAction, - onVoteAction, - onReplyEditAction, - onReportAction, - ) - : null, + if (widget.multiCommentCardActions[page]?.isNotEmpty == true) + MultiPickerItem( + pickerItems: [ + ...widget.multiCommentCardActions[page]!.where((a) => a.shouldShow?.call(context, widget.commentView) ?? true).map( + (a) { + return PickerItemData( + label: a.label, + icon: a.getOverrideIcon?.call(widget.commentView) ?? a.icon, + backgroundColor: a.color, + foregroundColor: a.getForegroundColor?.call(widget.commentView), + onSelected: (a.shouldEnable?.call(isUserLoggedIn) ?? true) ? () => onSelected(a.commentCardAction) : null, + ); + }, + ), + ], + ), + if (widget.commentCardActions[page]?.isNotEmpty == true) + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget.commentCardActions[page]!.length, + itemBuilder: (BuildContext itemBuilderContext, int index) { + return PickerItem( + label: widget.commentCardActions[page]![index].getOverrideLabel?.call(context, widget.commentView) ?? widget.commentCardActions[page]![index].label, + icon: widget.commentCardActions[page]![index].getOverrideIcon?.call(widget.commentView) ?? widget.commentCardActions[page]![index].icon, + trailingIcon: widget.commentCardActions[page]![index].trailingIcon, + onSelected: + (widget.commentCardActions[page]![index].shouldEnable?.call(isUserLoggedIn) ?? true) ? () => onSelected(widget.commentCardActions[page]![index].commentCardAction) : null, ); }, ), - ], - ), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: commentCardActionItems.length, - itemBuilder: (BuildContext itemBuilderContext, int index) { - return PickerItem( - label: commentCardActionItems[index].label, - icon: commentCardActionItems[index].icon, - onSelected: (commentCardActionItems[index].shouldEnable?.call(isUserLoggedIn) ?? true) - ? () => onSelected( - context, - commentCardActionItems[index].commentCardAction, - commentView, - onSaveAction, - onDeleteAction, - onVoteAction, - onReplyEditAction, - onReportAction, - ) - : null, - ); - }, - ), - const SizedBox(height: 16.0), - ], + const SizedBox(height: 16.0), + ], + ), ), - ); - }, - ); -} + ), + ); + } -void onSelected( - BuildContext context, - CommentCardAction commentCardAction, - CommentView commentView, - Function onSaveAction, - Function onDeleteAction, - Function onUpvoteAction, - Function onReplyEditAction, - Function onReportAction, -) async { - Navigator.of(context).pop(); - - switch (commentCardAction) { - case CommentCardAction.save: - onSaveAction(commentView.comment.id, !(commentView.saved)); - break; - case CommentCardAction.copyText: - Clipboard.setData(ClipboardData(text: commentView.comment.content)).then((_) { - showSnackbar(AppLocalizations.of(context)!.copiedToClipboard); - }); - break; - case CommentCardAction.shareLink: - Share.share(commentView.comment.apId); - break; - case CommentCardAction.delete: - onDeleteAction(commentView.comment.id, !(commentView.comment.deleted)); - case CommentCardAction.upvote: - onUpvoteAction(commentView.comment.id, commentView.myVote == 1 ? 0 : 1); - break; - case CommentCardAction.downvote: - onUpvoteAction(commentView.comment.id, commentView.myVote == -1 ? 0 : -1); - break; - case CommentCardAction.reply: - onReplyEditAction(commentView, false); - break; - case CommentCardAction.edit: - onReplyEditAction(commentView, true); - break; - case CommentCardAction.report: - onReportAction(commentView.comment.id); - break; - case CommentCardAction.visitProfile: - navigateToFeedPage(context, feedType: FeedType.user, userId: commentView.creator.id); - break; - case CommentCardAction.blockUser: - context.read().add(UserActionEvent(userAction: UserAction.block, userId: commentView.creator.id, value: true)); - break; - case CommentCardAction.visitInstance: - navigateToInstancePage(context, instanceHost: fetchInstanceNameFromUrl(commentView.creator.actorId)!, instanceId: commentView.community.instanceId); - break; - case CommentCardAction.blockInstance: - context.read().add(InstanceActionEvent( - instanceAction: InstanceAction.block, - instanceId: commentView.creator.instanceId, - domain: fetchInstanceNameFromUrl(commentView.creator.actorId), - value: true, - )); - break; + void onSelected(CommentCardAction commentCardAction) async { + bool pop = true; + Function action; + + switch (commentCardAction) { + case CommentCardAction.save: + action = () => widget.onSaveAction(widget.commentView.comment.id, !(widget.commentView.saved)); + break; + case CommentCardAction.copyText: + action = () => Clipboard.setData(ClipboardData(text: widget.commentView.comment.content)).then((_) { + showSnackbar(AppLocalizations.of(widget.outerContext)!.copiedToClipboard); + }); + break; + case CommentCardAction.shareLink: + action = () => Share.share(widget.commentView.comment.apId); + break; + case CommentCardAction.delete: + action = () => widget.onDeleteAction(widget.commentView.comment.id, !(widget.commentView.comment.deleted)); + case CommentCardAction.upvote: + action = () => widget.onVoteAction(widget.commentView.comment.id, widget.commentView.myVote == 1 ? 0 : 1); + break; + case CommentCardAction.downvote: + action = () => widget.onVoteAction(widget.commentView.comment.id, widget.commentView.myVote == -1 ? 0 : -1); + break; + case CommentCardAction.reply: + action = () => widget.onReplyEditAction(widget.commentView, false); + break; + case CommentCardAction.edit: + action = () => widget.onReplyEditAction(widget.commentView, true); + break; + case CommentCardAction.report: + action = () => widget.onReportAction(widget.commentView.comment.id); + break; + case CommentCardAction.userActions: + action = () => setState(() => page = CommentActionBottomSheetPage.user); + pop = false; + break; + case CommentCardAction.visitProfile: + action = () => navigateToFeedPage(widget.outerContext, feedType: FeedType.user, userId: widget.commentView.creator.id); + break; + case CommentCardAction.blockUser: + action = () => widget.outerContext.read().add(UserActionEvent(userAction: UserAction.block, userId: widget.commentView.creator.id, value: true)); + break; + case CommentCardAction.instanceActions: + action = () => setState(() => page = CommentActionBottomSheetPage.instance); + pop = false; + + case CommentCardAction.visitInstance: + action = () => navigateToInstancePage(widget.outerContext, instanceHost: fetchInstanceNameFromUrl(widget.commentView.creator.actorId)!, instanceId: widget.commentView.community.instanceId); + break; + case CommentCardAction.blockInstance: + action = () => widget.outerContext.read().add(InstanceActionEvent( + instanceAction: InstanceAction.block, + instanceId: widget.commentView.creator.instanceId, + domain: fetchInstanceNameFromUrl(widget.commentView.creator.actorId), + value: true, + )); + break; + } + + if (pop) { + Navigator.of(context).pop(); + } + + action(); } -} -List _updateDefaultCommentActionItems(BuildContext context, CommentView commentView) { - final bool isOwnComment = commentView.creator.id == context.read().state.account?.userId; - bool isDeleted = commentView.comment.deleted; - List updatedList = [...commentCardDefaultActionItems]; + FutureOr _handleBack(bool stopDefaultButtonEvent, RouteInfo routeInfo) { + if (page != CommentActionBottomSheetPage.general) { + setState(() => page = CommentActionBottomSheetPage.general); + return true; + } - if (isOwnComment) { - updatedList.add(ExtendedCommentCardActions( - commentCardAction: CommentCardAction.delete, - icon: isDeleted ? Icons.restore_from_trash_rounded : Icons.delete_rounded, - label: isDeleted ? AppLocalizations.of(GlobalContext.context)!.restore : AppLocalizations.of(GlobalContext.context)!.delete, - )); + return false; } - return updatedList; } void showReportCommentActionBottomSheet( diff --git a/lib/post/widgets/post_view.dart b/lib/post/widgets/post_view.dart index 47bec1e74..a1598ca96 100644 --- a/lib/post/widgets/post_view.dart +++ b/lib/post/widgets/post_view.dart @@ -510,17 +510,14 @@ class _PostSubviewState extends State with SingleTickerProviderStat Expanded( flex: 1, child: IconButton( - icon: const Icon(Icons.share_rounded, semanticLabel: 'Share'), - onPressed: useAdvancedShareSheet - ? () => showAdvancedShareSheet(context, widget.postViewMedia) - : widget.postViewMedia.media.isEmpty - ? () => Share.share(post.apId) - : () => showPostActionBottomModalSheet( - context, - widget.postViewMedia, - actionsToInclude: [PostCardAction.sharePost, PostCardAction.shareMedia, PostCardAction.shareLink], - ), - ), + icon: const Icon(Icons.share_rounded, semanticLabel: 'Share'), + onPressed: () { + if (useAdvancedShareSheet) { + showAdvancedShareSheet(context, widget.postViewMedia); + } else { + showPostActionBottomModalSheet(context, widget.postViewMedia, page: PostActionBottomSheetPage.share); + } + }), ) ], ), diff --git a/lib/shared/picker_item.dart b/lib/shared/picker_item.dart index 2e0909820..855ef2c50 100644 --- a/lib/shared/picker_item.dart +++ b/lib/shared/picker_item.dart @@ -36,6 +36,8 @@ class PickerItem extends StatelessWidget { child: ListTile( title: Text( label, + softWrap: false, + overflow: TextOverflow.fade, style: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.copyWith( color: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.color?.withOpacity(onSelected == null ? 0.5 : 1), ), From dd9122122fe6d7397adc9cf73587f968b69894c4 Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Thu, 14 Mar 2024 10:28:46 -0400 Subject: [PATCH 2/2] Put entity in subtitle --- lib/community/utils/post_card_action_helpers.dart | 15 +++++++++------ lib/post/utils/comment_action_helpers.dart | 11 +++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/community/utils/post_card_action_helpers.dart b/lib/community/utils/post_card_action_helpers.dart index 93b469b38..501a86fe5 100644 --- a/lib/community/utils/post_card_action_helpers.dart +++ b/lib/community/utils/post_card_action_helpers.dart @@ -73,6 +73,7 @@ class ExtendedPostCardActions { this.getForegroundColor, this.getOverrideIcon, this.getOverrideLabel, + this.getSubtitleLabel, this.shouldShow, this.shouldEnable, }); @@ -85,6 +86,7 @@ class ExtendedPostCardActions { final Color? Function(PostView postView)? getForegroundColor; final IconData? Function(PostView postView)? getOverrideIcon; final String? Function(BuildContext context, PostView postView)? getOverrideLabel; + final String? Function(BuildContext context, PostView postView)? getSubtitleLabel; final bool Function(BuildContext context, PostView commentView)? shouldShow; final bool Function(bool isUserLoggedIn)? shouldEnable; } @@ -95,8 +97,8 @@ final List postCardActionItems = [ ExtendedPostCardActions( postCardAction: PostCardAction.userActions, icon: Icons.person_rounded, - label: '', - getOverrideLabel: (context, postView) => l10n.userEntry(generateUserFullName(context, postView.creator.name, fetchInstanceNameFromUrl(postView.creator.actorId))), + label: l10n.user, + getSubtitleLabel: (context, postView) => generateUserFullName(context, postView.creator.name, fetchInstanceNameFromUrl(postView.creator.actorId)), trailingIcon: Icons.chevron_right_rounded, ), ExtendedPostCardActions( @@ -113,8 +115,8 @@ final List postCardActionItems = [ ExtendedPostCardActions( postCardAction: PostCardAction.communityActions, icon: Icons.people_rounded, - label: '', - getOverrideLabel: (context, postView) => l10n.communityEntry(generateCommunityFullName(context, postView.community.name, fetchInstanceNameFromUrl(postView.community.actorId))), + label: l10n.community, + getSubtitleLabel: (context, postView) => generateCommunityFullName(context, postView.community.name, fetchInstanceNameFromUrl(postView.community.actorId)), trailingIcon: Icons.chevron_right_rounded, ), ExtendedPostCardActions( @@ -143,8 +145,8 @@ final List postCardActionItems = [ ExtendedPostCardActions( postCardAction: PostCardAction.instanceActions, icon: Icons.language_rounded, - label: '', - getOverrideLabel: (context, postView) => l10n.instanceEntry(fetchInstanceNameFromUrl(postView.community.actorId) ?? ''), + label: l10n.instance(1), + getSubtitleLabel: (context, postView) => fetchInstanceNameFromUrl(postView.community.actorId) ?? '', trailingIcon: Icons.chevron_right_rounded, ), ExtendedPostCardActions( @@ -509,6 +511,7 @@ class _PostCardActionPickerState extends State { 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.postView), 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) diff --git a/lib/post/utils/comment_action_helpers.dart b/lib/post/utils/comment_action_helpers.dart index 0e7b9db68..77fd7f34b 100644 --- a/lib/post/utils/comment_action_helpers.dart +++ b/lib/post/utils/comment_action_helpers.dart @@ -54,6 +54,7 @@ class ExtendedCommentCardActions { this.getForegroundColor, this.getOverrideIcon, this.getOverrideLabel, + this.getSubtitleLabel, this.shouldShow, this.shouldEnable, }); @@ -66,6 +67,7 @@ class ExtendedCommentCardActions { final Color? Function(CommentView commentView)? getForegroundColor; final IconData? Function(CommentView commentView)? getOverrideIcon; final String Function(BuildContext context, CommentView commentView)? getOverrideLabel; + final String Function(BuildContext context, CommentView commentView)? getSubtitleLabel; final bool Function(BuildContext context, CommentView commentView)? shouldShow; final bool Function(bool isUserLoggedIn)? shouldEnable; } @@ -76,8 +78,8 @@ final List commentCardDefaultActionItems = [ ExtendedCommentCardActions( commentCardAction: CommentCardAction.userActions, icon: Icons.person_rounded, - label: '', - getOverrideLabel: (context, commentView) => l10n.userEntry(generateUserFullName(context, commentView.creator.name, fetchInstanceNameFromUrl(commentView.creator.actorId))), + label: l10n.user, + getSubtitleLabel: (context, commentView) => generateUserFullName(context, commentView.creator.name, fetchInstanceNameFromUrl(commentView.creator.actorId)), trailingIcon: Icons.chevron_right_rounded, ), ExtendedCommentCardActions( @@ -94,8 +96,8 @@ final List commentCardDefaultActionItems = [ ExtendedCommentCardActions( commentCardAction: CommentCardAction.instanceActions, icon: Icons.language_rounded, - label: '', - getOverrideLabel: (context, postView) => l10n.instanceEntry(fetchInstanceNameFromUrl(postView.creator.actorId) ?? ''), + label: l10n.instance(1), + getSubtitleLabel: (context, postView) => fetchInstanceNameFromUrl(postView.creator.actorId) ?? '', trailingIcon: Icons.chevron_right_rounded, ), ExtendedCommentCardActions( @@ -388,6 +390,7 @@ class _CommentActionPickerState extends State { itemBuilder: (BuildContext itemBuilderContext, int index) { return PickerItem( label: widget.commentCardActions[page]![index].getOverrideLabel?.call(context, widget.commentView) ?? widget.commentCardActions[page]![index].label, + subtitle: widget.commentCardActions[page]![index].getSubtitleLabel?.call(context, widget.commentView), icon: widget.commentCardActions[page]![index].getOverrideIcon?.call(widget.commentView) ?? widget.commentCardActions[page]![index].icon, trailingIcon: widget.commentCardActions[page]![index].trailingIcon, onSelected: