Skip to content

Commit

Permalink
Comment bottom sheet redesign (#1654)
Browse files Browse the repository at this point in the history
  • Loading branch information
hjiangsu authored Jan 16, 2025
1 parent fbfc278 commit e6b2e0b
Show file tree
Hide file tree
Showing 31 changed files with 1,289 additions and 1,193 deletions.
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

0 comments on commit e6b2e0b

Please sign in to comment.