From 0e25a8ea3f4bbec6b89d8af939cfd64681940327 Mon Sep 17 00:00:00 2001 From: ajsosa Date: Sat, 9 Sep 2023 17:25:55 -0500 Subject: [PATCH] Creating comments now inserts the comment into the comment tree instead of refreshing page. The newly created comment is also highlighted. When commenting from a comment context view, the highlighted comment changes to the newly created comment. If the comment is a top level comment, then it gets inserted at the very top and the scroll controller is then scrolled to the top. If the comment is a direct reply, it gets appended immediately under the parent and the scroll position is maintained. --- lib/post/bloc/post_bloc.dart | 45 ++++++++++++++------------- lib/post/bloc/post_event.dart | 3 +- lib/post/bloc/post_state.dart | 7 +++++ lib/post/pages/post_page.dart | 1 + lib/post/pages/post_page_success.dart | 3 ++ lib/post/widgets/comment_card.dart | 17 +++++----- lib/post/widgets/comment_view.dart | 10 ++++++ lib/utils/comment.dart | 43 +++++++++++++++++++++++++ 8 files changed, 100 insertions(+), 29 deletions(-) diff --git a/lib/post/bloc/post_bloc.dart b/lib/post/bloc/post_bloc.dart index 20e4335bb..c45c7db63 100644 --- a/lib/post/bloc/post_bloc.dart +++ b/lib/post/bloc/post_bloc.dart @@ -90,7 +90,8 @@ class PostBloc extends Bloc { while (attemptCount < 2) { try { - emit(state.copyWith(status: PostStatus.loading, selectedCommentPath: event.selectedCommentPath, selectedCommentId: event.selectedCommentId)); + emit(state.copyWith( + status: PostStatus.loading, selectedCommentPath: event.selectedCommentPath, selectedCommentId: event.selectedCommentId, newlyCreatedCommentId: event.newlyCreatedCommentId)); LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; @@ -126,18 +127,18 @@ class PostBloc extends Bloc { } } - emit( - state.copyWith( - status: PostStatus.success, - postId: postView?.postView.post.id, - postView: postView, - communityId: postView?.postView.post.communityId, - moderators: moderators, - selectedCommentPath: event.selectedCommentPath, - selectedCommentId: event.selectedCommentId), - ); + emit(state.copyWith( + status: PostStatus.success, + postId: postView?.postView.post.id, + postView: postView, + communityId: postView?.postView.post.communityId, + moderators: moderators, + selectedCommentPath: event.selectedCommentPath, + selectedCommentId: event.selectedCommentId, + newlyCreatedCommentId: event.newlyCreatedCommentId)); - emit(state.copyWith(status: PostStatus.refreshing, selectedCommentPath: event.selectedCommentPath, selectedCommentId: event.selectedCommentId)); + emit(state.copyWith( + status: PostStatus.refreshing, selectedCommentPath: event.selectedCommentPath, selectedCommentId: event.selectedCommentId, newlyCreatedCommentId: event.newlyCreatedCommentId)); CommentSortType sortType = event.sortType ?? (state.sortType ?? defaultSortType); @@ -183,7 +184,8 @@ class PostBloc extends Bloc { communityId: postView?.postView.post.communityId, sortType: sortType, selectedCommentId: event.selectedCommentId, - selectedCommentPath: event.selectedCommentPath), + selectedCommentPath: event.selectedCommentPath, + newlyCreatedCommentId: event.newlyCreatedCommentId), ); } catch (e) { exception = e; @@ -469,17 +471,18 @@ class PostBloc extends Bloc { int? selectedCommentId = event.selectedCommentId; String? selectedCommentPath = event.selectedCommentPath; - // for now, refresh the post and refetch the comments - // @todo: insert the new comment in place without requiring a refetch - // @todo: alternatively, insert and scroll to new comment on refetch - if (event.parentCommentId != null) { - add(GetPostEvent(postView: state.postView!, selectedCommentId: selectedCommentId, selectedCommentPath: selectedCommentPath)); - } else { + List updatedComments = insertNewComment(state.comments, createComment.commentView); + + if (event.parentCommentId == null) { selectedCommentId = null; selectedCommentPath = null; - add(GetPostEvent(postView: state.postView!, selectedCommentId: selectedCommentId, selectedCommentPath: selectedCommentPath)); } - return emit(state.copyWith(status: PostStatus.success, selectedCommentId: selectedCommentId, selectedCommentPath: selectedCommentPath)); + return emit(state.copyWith( + status: PostStatus.success, + comments: updatedComments, + selectedCommentId: selectedCommentId, + selectedCommentPath: selectedCommentPath, + newlyCreatedCommentId: createComment.commentView.comment.id)); } catch (e) { return emit(state.copyWith(status: PostStatus.failure, errorMessage: e.toString())); } diff --git a/lib/post/bloc/post_event.dart b/lib/post/bloc/post_event.dart index 4774f1164..4df535fb4 100644 --- a/lib/post/bloc/post_event.dart +++ b/lib/post/bloc/post_event.dart @@ -13,8 +13,9 @@ class GetPostEvent extends PostEvent { final CommentSortType? sortType; final String? selectedCommentPath; final int? selectedCommentId; + final int? newlyCreatedCommentId; - const GetPostEvent({this.sortType, this.postView, this.postId, this.selectedCommentPath, this.selectedCommentId}); + const GetPostEvent({this.sortType, this.postView, this.postId, this.selectedCommentPath, this.selectedCommentId, this.newlyCreatedCommentId}); } class GetPostCommentsEvent extends PostEvent { diff --git a/lib/post/bloc/post_state.dart b/lib/post/bloc/post_state.dart index c2910ba8a..e6ef069b2 100644 --- a/lib/post/bloc/post_state.dart +++ b/lib/post/bloc/post_state.dart @@ -19,6 +19,7 @@ class PostState extends Equatable { this.sortTypeIcon, this.selectedCommentId, this.selectedCommentPath, + this.newlyCreatedCommentId, this.moddingCommentId = -1, this.viewAllCommentsRefresh = false, this.navigateCommentIndex = 0, @@ -44,7 +45,9 @@ class PostState extends Equatable { final int commentCount; final bool hasReachedCommentEnd; final int? selectedCommentId; + final int? newlyCreatedCommentId; final String? selectedCommentPath; + // This is to track what comment is being restored or deleted so we can // show a spinner indicator that thunder is working on it final int moddingCommentId; @@ -52,6 +55,7 @@ class PostState extends Equatable { final String? errorMessage; final int navigateCommentIndex; + // This exists purely for forcing the bloc to refire // even if the comment index doesn't change final int navigateCommentId; @@ -72,6 +76,7 @@ class PostState extends Equatable { IconData? sortTypeIcon, int? selectedCommentId, String? selectedCommentPath, + int? newlyCreatedCommentId, int? moddingCommentId, bool? viewAllCommentsRefresh = false, int? navigateCommentIndex, @@ -93,6 +98,7 @@ class PostState extends Equatable { sortTypeIcon: sortTypeIcon ?? this.sortTypeIcon, selectedCommentId: selectedCommentId, selectedCommentPath: selectedCommentPath, + newlyCreatedCommentId: newlyCreatedCommentId, moddingCommentId: moddingCommentId ?? this.moddingCommentId, viewAllCommentsRefresh: viewAllCommentsRefresh ?? false, navigateCommentIndex: navigateCommentIndex ?? 0, @@ -116,6 +122,7 @@ class PostState extends Equatable { sortTypeIcon, selectedCommentId, selectedCommentPath, + newlyCreatedCommentId, viewAllCommentsRefresh, moddingCommentId, navigateCommentIndex, diff --git a/lib/post/pages/post_page.dart b/lib/post/pages/post_page.dart index 588ea43c9..1f1581592 100644 --- a/lib/post/pages/post_page.dart +++ b/lib/post/pages/post_page.dart @@ -388,6 +388,7 @@ class _PostPageState extends State { comments: state.comments, selectedCommentId: state.selectedCommentId, selectedCommentPath: state.selectedCommentPath, + newlyCreatedCommentId: state.newlyCreatedCommentId, moddingCommentId: state.moddingCommentId, viewFullCommentsRefreshing: state.viewAllCommentsRefresh, itemScrollController: _itemScrollController, diff --git a/lib/post/pages/post_page_success.dart b/lib/post/pages/post_page_success.dart index e15cef879..2ae8499a8 100644 --- a/lib/post/pages/post_page_success.dart +++ b/lib/post/pages/post_page_success.dart @@ -28,6 +28,7 @@ class PostPageSuccess extends StatefulWidget { final List comments; final int? selectedCommentId; final String? selectedCommentPath; + final int? newlyCreatedCommentId; final int? moddingCommentId; final ItemScrollController itemScrollController; @@ -47,6 +48,7 @@ class PostPageSuccess extends StatefulWidget { this.hasReachedCommentEnd = false, this.selectedCommentId, this.selectedCommentPath, + this.newlyCreatedCommentId, this.moddingCommentId, this.viewFullCommentsRefreshing = false, required this.moderators, @@ -85,6 +87,7 @@ class _PostPageSuccessState extends State { moddingCommentId: widget.moddingCommentId, selectedCommentId: widget.selectedCommentId, selectedCommentPath: widget.selectedCommentPath, + newlyCreatedCommentId: widget.newlyCreatedCommentId, now: DateTime.now().toUtc(), itemScrollController: widget.itemScrollController, itemPositionsListener: widget.itemPositionsListener, diff --git a/lib/post/widgets/comment_card.dart b/lib/post/widgets/comment_card.dart index c3f2b3fbe..78ffd75eb 100644 --- a/lib/post/widgets/comment_card.dart +++ b/lib/post/widgets/comment_card.dart @@ -9,9 +9,6 @@ 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_actions.dart'; -import 'package:thunder/shared/comment_card_actions.dart'; -import 'package:thunder/shared/comment_header.dart'; -import 'package:thunder/shared/common_markdown_body.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/models/comment_view_tree.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; @@ -29,6 +26,7 @@ class CommentCard extends StatefulWidget { final Set collapsedCommentSet; final int? selectCommentId; final String? selectedCommentPath; + final int? newlyCreatedCommentId; final int? moddingCommentId; final DateTime now; @@ -48,6 +46,7 @@ class CommentCard extends StatefulWidget { this.collapsedCommentSet = const {}, this.selectCommentId, this.selectedCommentPath, + this.newlyCreatedCommentId, this.moddingCommentId, required this.onDeleteAction, required this.moderators, @@ -140,12 +139,15 @@ class _CommentCardState extends State with SingleTickerProviderStat // Checks for the same creator id to user id final bool isOwnComment = widget.commentViewTree.commentView?.creator.id == context.read().state.account?.userId; - final bool isUserLoggedIn = context.read().state.isLoggedIn; - final ThunderState state = context.read().state; - bool collapseParentCommentOnGesture = state.collapseParentCommentOnGesture; + final int? commentId = widget.commentViewTree.commentView?.comment.id; + bool highlightComment = false; + if (widget.selectCommentId == commentId && widget.newlyCreatedCommentId == null || widget.newlyCreatedCommentId == commentId) { + highlightComment = true; + } + NestedCommentIndicatorStyle nestedCommentIndicatorStyle = state.nestedCommentIndicatorStyle; NestedCommentIndicatorColor nestedCommentIndicatorColor = state.nestedCommentIndicatorColor; @@ -335,7 +337,7 @@ class _CommentCardState extends State with SingleTickerProviderStat ), ), child: Material( - color: widget.selectCommentId == widget.commentViewTree.commentView!.comment.id ? theme.highlightColor : theme.colorScheme.background, + color: highlightComment ? theme.highlightColor : theme.colorScheme.background, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, @@ -442,6 +444,7 @@ class _CommentCardState extends State with SingleTickerProviderStat moddingCommentId: widget.moddingCommentId, selectedCommentPath: widget.selectedCommentPath, selectCommentId: widget.selectCommentId, + newlyCreatedCommentId: widget.newlyCreatedCommentId, now: widget.now, commentViewTree: widget.commentViewTree.replies[index], collapsedCommentSet: widget.collapsedCommentSet, diff --git a/lib/post/widgets/comment_view.dart b/lib/post/widgets/comment_view.dart index 6b56b877b..21d894678 100644 --- a/lib/post/widgets/comment_view.dart +++ b/lib/post/widgets/comment_view.dart @@ -25,6 +25,7 @@ class CommentSubview extends StatefulWidget { final PostViewMedia? postViewMedia; final int? selectedCommentId; final String? selectedCommentPath; + final int? newlyCreatedCommentId; final int? moddingCommentId; final ItemScrollController itemScrollController; final ItemPositionsListener itemPositionsListener; @@ -46,6 +47,7 @@ class CommentSubview extends StatefulWidget { this.postViewMedia, this.selectedCommentId, this.selectedCommentPath, + this.newlyCreatedCommentId, this.moddingCommentId, required this.itemScrollController, required this.itemPositionsListener, @@ -107,6 +109,13 @@ class _CommentSubviewState extends State with SingleTickerProvid duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, ); + } else if (state.newlyCreatedCommentId != null && state.comments.first.commentView?.comment.id == state.newlyCreatedCommentId) { + // Only scroll for top level comments since you can comment from anywhere in the comment section. + widget.itemScrollController.scrollTo( + index: 1, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); } }, child: ScrollablePositionedList.builder( @@ -172,6 +181,7 @@ class _CommentSubviewState extends State with SingleTickerProvid now: widget.now, selectCommentId: widget.selectedCommentId, selectedCommentPath: widget.selectedCommentPath, + newlyCreatedCommentId: widget.newlyCreatedCommentId, moddingCommentId: widget.moddingCommentId, commentViewTree: widget.comments[index - 1], collapsedCommentSet: collapsedCommentSet, diff --git a/lib/utils/comment.dart b/lib/utils/comment.dart index f0a107473..a498e52c1 100644 --- a/lib/utils/comment.dart +++ b/lib/utils/comment.dart @@ -103,6 +103,49 @@ List buildCommentViewTree(List comments, {bool fla return commentMap.values.where((commentView) => commentView.commentView!.comment.path.isEmpty || commentView.commentView!.comment.path == '0.${commentView.commentView!.comment.id}').toList(); } +List insertNewComment(List comments, CommentView commentView) { + List parentIds = commentView.comment.path.split('.'); + String commentTime = commentView.comment.published.toIso8601String(); + + CommentViewTree newCommentTree = CommentViewTree( + datePostedOrEdited: formatTimeToString(dateTime: commentTime), + commentView: commentView, + replies: [], + level: commentView.comment.path.split('.').length - 2, + ); + + if (parentIds[1] == commentView.comment.id.toString()) { + comments.insert(0, newCommentTree); + return comments; + } + + String parentId = parentIds[parentIds.length - 2]; + CommentViewTree? parentComment = findParentComment(1, parentIds, parentId.toString(), comments); + + // TODO: surface some sort of error maybe if for some reason we fail to find parent comment + if (parentComment != null) { + parentComment.replies.insert(0, newCommentTree); + } + + return comments; +} + +CommentViewTree? findParentComment(int index, List parentIds, String targetId, List comments) { + for (CommentViewTree existing in comments) { + if (existing.commentView?.comment.id.toString() != parentIds[index]) { + continue; + } + + if (targetId == existing.commentView?.comment.id.toString()) { + return existing; + } + + return findParentComment(index + 1, parentIds, targetId, existing.replies); + } + + return null; +} + List findCommentIndexesFromCommentViewTree(List commentTrees, int commentId, [List? indexes]) { indexes ??= [];