Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comment bottom sheet redesign #1654

Merged
merged 10 commits into from
Jan 16, 2025
4 changes: 4 additions & 0 deletions lib/comment/enums/comment_action.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import 'package:thunder/post/enums/post_action.dart';

enum CommentAction {
viewSource(permissionType: PermissionType.all),

/// User level comment actions
vote(permissionType: PermissionType.user),
save(permissionType: PermissionType.user),
delete(permissionType: PermissionType.user),
report(permissionType: PermissionType.user),
reply(permissionType: PermissionType.user),
edit(permissionType: PermissionType.user),
read(permissionType: PermissionType.user), // This is used for inbox items (replies/mentions)

/// Moderator level post actions
Expand Down
1 change: 0 additions & 1 deletion lib/comment/view/create_comment_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,6 @@ class _CreateCommentPageState extends State<CreateCommentPage> {
onVoteAction: (_, __) {},
onSaveAction: (_, __) {},
onReplyEditAction: (_, __) {},
onReportAction: (_) {},
onDeleteAction: (_, __) {},
isUserLoggedIn: true,
isOwnComment: false,
Expand Down
182 changes: 182 additions & 0 deletions lib/comment/widgets/comment_action_bottom_sheet.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import 'dart:async';

import 'package:flutter/material.dart';

import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:lemmy_api_client/v3.dart';

import 'package:thunder/comment/enums/comment_action.dart';
import 'package:thunder/comment/widgets/comment_comment_action_bottom_sheet.dart';
import 'package:thunder/comment/widgets/general_comment_action_bottom_sheet.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/instance/widgets/instance_action_bottom_sheet.dart';
import 'package:thunder/shared/share/share_action_bottom_sheet.dart';
import 'package:thunder/user/enums/user_action.dart';
import 'package:thunder/user/widgets/user_action_bottom_sheet.dart';
import 'package:thunder/utils/instance.dart';

/// Programatically show the comment action bottom sheet
void showCommentActionBottomModalSheet(
BuildContext context,
CommentView commentView, {
bool isShowingSource = false,
GeneralCommentAction page = GeneralCommentAction.general,
void Function({CommentAction? commentAction, UserAction? userAction, CommunityAction? communityAction, required CommentView commentView, dynamic value})? onAction,
}) {
showModalBottomSheet(
context: context,
showDragHandle: true,
isScrollControlled: true,
builder: (_) => CommentActionBottomSheet(context: context, initialPage: page, commentView: commentView, onAction: onAction, isShowingSource: isShowingSource),
);
}

class CommentActionBottomSheet extends StatefulWidget {
const CommentActionBottomSheet({super.key, required this.context, required this.commentView, this.initialPage = GeneralCommentAction.general, required this.onAction, this.isShowingSource = false});

/// The parent context
final BuildContext context;

/// The comment that is being acted on
final CommentView commentView;

/// Whether the source of the comment is being shown
final bool isShowingSource;

/// The initial page of the bottom sheet
final GeneralCommentAction initialPage;

/// The callback that is called when an action is performed
final void Function({CommentAction? commentAction, UserAction? userAction, CommunityAction? communityAction, required CommentView commentView, dynamic value})? onAction;

@override
State<CommentActionBottomSheet> createState() => _CommentActionBottomSheetState();
}

class _CommentActionBottomSheetState extends State<CommentActionBottomSheet> {
GeneralCommentAction currentPage = GeneralCommentAction.general;

FutureOr<bool> _handleBack(bool stopDefaultButtonEvent, RouteInfo routeInfo) {
if (currentPage != GeneralCommentAction.general) {
setState(() => currentPage = GeneralCommentAction.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(GeneralCommentAction page) {
CommentView commentView = widget.commentView;

String? communityInstance = fetchInstanceNameFromUrl(commentView.community.actorId);
String? userInstance = fetchInstanceNameFromUrl(commentView.creator.actorId);

switch (page) {
case GeneralCommentAction.user:
return generateUserFullName(context, commentView.creator.name, commentView.creator.displayName, fetchInstanceNameFromUrl(commentView.creator.actorId));
case GeneralCommentAction.instance:
return (communityInstance == userInstance) ? '$communityInstance' : '$communityInstance • $userInstance';
default:
return null;
}
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);

Widget actions = switch (currentPage) {
GeneralCommentAction.general => GeneralCommentActionBottomSheetPage(
context: widget.context,
commentView: widget.commentView,
onSwitchActivePage: (page) => setState(() => currentPage = page),
onAction: (CommentAction commentAction, CommentView? updatedCommentView, dynamic value) {
widget.onAction?.call(commentAction: commentAction, commentView: widget.commentView, value: value);
},
),
GeneralCommentAction.comment => CommentCommentActionBottomSheet(
context: widget.context,
commentView: widget.commentView,
isShowingSource: widget.isShowingSource,
onAction: (CommentAction commentAction, CommentView? updatedCommentView, dynamic value) {
widget.onAction?.call(commentAction: commentAction, commentView: widget.commentView, value: value);
},
),
GeneralCommentAction.user => UserActionBottomSheet(
context: widget.context,
user: widget.commentView.creator,
communityId: widget.commentView.community.id,
isUserCommunityModerator: widget.commentView.creatorIsModerator,
isUserBannedFromCommunity: widget.commentView.creatorBannedFromCommunity,
onAction: (UserAction userAction, PersonView? updatedPersonView) {
widget.onAction?.call(userAction: userAction, commentView: widget.commentView);
},
),
GeneralCommentAction.instance => InstanceActionBottomSheet(
userInstanceId: widget.commentView.creator.instanceId,
userInstanceUrl: widget.commentView.creator.actorId,
onAction: () {},
),
GeneralCommentAction.share => ShareActionBottomSheet(
context: widget.context,
commentView: widget.commentView,
onAction: () {},
),
};

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 != GeneralCommentAction.general
? IconButton(onPressed: () => setState(() => currentPage = GeneralCommentAction.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 != GeneralCommentAction.general && currentPage != GeneralCommentAction.share && currentPage != GeneralCommentAction.comment)
Text(generateSubtitle(currentPage) ?? ''),
],
),
],
),
if (currentPage == GeneralCommentAction.general)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
child: LanguagePostCardMetaData(languageId: widget.commentView.comment.languageId),
),
const SizedBox(height: 16.0),
actions,
],
),
),
),
);
}
}
56 changes: 36 additions & 20 deletions lib/comment/widgets/comment_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:thunder/comment/utils/navigate_comment.dart';

import 'package:thunder/comment/enums/comment_action.dart';
import 'package:thunder/comment/utils/navigate_comment.dart';
import 'package:thunder/comment/widgets/comment_action_bottom_sheet.dart';
import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/core/enums/nested_comment_indicator.dart';
import 'package:thunder/core/enums/swipe_action.dart';
import 'package:thunder/post/bloc/post_bloc.dart';
import 'package:thunder/post/utils/comment_action_helpers.dart';
import 'package:thunder/post/utils/comment_actions.dart';
import 'package:thunder/shared/comment_content.dart';
import 'package:thunder/shared/text/scalable_text.dart';
Expand Down Expand Up @@ -57,9 +58,6 @@ class CommentCard extends StatefulWidget {
/// Callback function for when a comment being replied to or edited
final Function(CommentView commentView, bool isEdit)? onReplyEditAction;

/// Callback function for when a comment is reported
final Function(int commentId)? onReportAction;

const CommentCard({
super.key,
required this.commentView,
Expand All @@ -75,7 +73,6 @@ class CommentCard extends StatefulWidget {
this.onCollapseCommentChange,
this.onDeleteAction,
this.onReplyEditAction,
this.onReportAction,
});

@override
Expand Down Expand Up @@ -306,20 +303,40 @@ class _CommentCardState extends State<CommentCard> with SingleTickerProviderStat
showCommentActionBottomModalSheet(
context,
widget.commentView,
widget.onSaveAction ?? () {},
widget.onDeleteAction ?? () {},
widget.onVoteAction ?? () {},
(CommentView commentView, bool isEdit) {
return navigateToCreateCommentPage(
context,
commentView: isEdit ? commentView : null,
parentCommentView: isEdit ? null : commentView,
onCommentSuccess: (commentView, isEdit) => widget.onReplyEditAction?.call(commentView, isEdit),
);
isShowingSource: viewSource,
onAction: ({commentAction, required commentView, communityAction, userAction, value}) {
if (commentAction != null) {
switch (commentAction) {
case CommentAction.vote:
widget.onVoteAction?.call(commentView.comment.id, value);
break;
case CommentAction.save:
widget.onSaveAction?.call(commentView.comment.id, value);
break;
case CommentAction.reply:
widget.onReplyEditAction?.call(commentView, false);
break;
case CommentAction.edit:
widget.onReplyEditAction?.call(commentView, true);
break;
case CommentAction.delete:
widget.onDeleteAction?.call(commentView.comment.id, value);
break;
case CommentAction.report:
context.read<PostBloc>().add(ReportCommentEvent(commentId: commentView.comment.id, message: value));
break;
case CommentAction.viewSource:
setState(() => viewSource = !viewSource);
break;
default:
break;
}
} else if (communityAction != null) {
// @todo - implement community actions
} else if (userAction != null) {
setState(() {});
}
},
widget.onReportAction ?? () {},
() => setState(() => viewSource = !viewSource),
viewSource,
);
},
onTap: () {
Expand All @@ -331,7 +348,6 @@ class _CommentCardState extends State<CommentCard> with SingleTickerProviderStat
onSaveAction: (int commentId, bool save) => widget.onSaveAction?.call(commentId, save),
onVoteAction: (int commentId, int vote) => widget.onVoteAction?.call(commentId, vote),
onDeleteAction: (int commentId, bool deleted) => widget.onDeleteAction?.call(commentId, deleted),
onReportAction: (int commentId) => widget.onReportAction?.call(commentId),
onReplyEditAction: (CommentView commentView, bool isEdit) {
return navigateToCreateCommentPage(
context,
Expand Down
Loading
Loading