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

Refactor Post/Comment page #1363

Merged
merged 17 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions lib/comment/enums/comment_action.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:thunder/post/enums/post_action.dart';

enum CommentAction {
/// User level comment actions
vote(permissionType: PermissionType.user),
save(permissionType: PermissionType.user),
delete(permissionType: PermissionType.user),
report(permissionType: PermissionType.user),

/// Moderator level post actions
remove(permissionType: PermissionType.moderator),

/// Admin level post actions
purge(permissionType: PermissionType.admin);

const CommentAction({
required this.permissionType,
});

final PermissionType permissionType;
}
92 changes: 92 additions & 0 deletions lib/comment/models/comment_node.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:lemmy_api_client/v3.dart';

/// A node representing a single comment. This node can be part of a [CommentNode] tree.
///
/// The root node is defined by having a null [commentView]
class CommentNode {
/// The comment information associated with this node. If this is the root node, this will be null
final CommentView? commentView;

/// The replies to this comment
final List<CommentNode> replies;

/// Gets the depth/level of the comment in the tree. A depth of 0 indicates a root comment.
/// The [commentView.comment.path] is a dot-separated string of comment ids starting from 0 (post). For example: `0.103315`
int get depth {
if (commentView == null) return 0;

List<String> pathSegments = commentView!.comment.path.split('.');
int depth = pathSegments.length > 2 ? pathSegments.length - 2 : 0;

return depth;
}

/// Gets the total number of replies
get totalReplies => replies.length;

CommentNode({this.commentView, this.replies = const []});

/// Adds a reply to this comment node
/// There is a constraint where the comment [id] must be unique. If there exists a comment that has the same [id], we will replace it with the new comment.
void addReply(CommentNode reply) {
// Add the comment only if theres no other comment with the same id
int existingCommentNodeIndex = replies.indexWhere((node) => node.commentView?.comment.id == reply.commentView?.comment.id);

if (existingCommentNodeIndex != -1) {
// Replace the comment with the new comment
replies[existingCommentNodeIndex] = reply;
return;
}

replies.add(reply);
}

/// A static helper method to insert a comment node into the tree.
/// If the parent node is not found, the comment node is added to the root node.
static void insertCommentNode(CommentNode root, String parentId, CommentNode commentNode) {
CommentNode? parent = findCommentNode(root, parentId);

if (parent == null) {
return root.addReply(commentNode);
}

parent.addReply(commentNode);
}

/// A static helper method to find a comment node in the tree given its [id]. The [id] comes from [comment.path]
/// Returns null if the node is not found.
static CommentNode? findCommentNode(CommentNode node, String id) {
String? nodeId = node.commentView?.comment.path.split('.').last;

// Return the current node if it's the target
if (nodeId == id) return node;

// Recursively search for the target node
for (CommentNode child in node.replies) {
CommentNode? found = findCommentNode(child, id);
if (found != null) return found;
}

return null;
}

/// A static helper method to flatten a [CommentNode] tree. This flattens the tree using DFS.
/// DFS allows us to preserve the order of the comments in the tree.
///
/// Returns a list of flattened nodes.
static List<CommentNode> flattenCommentTree(CommentNode? root) {
List<CommentNode> flattenedCommentNodes = [];
if (root == null) return flattenedCommentNodes;

void flatten(CommentNode node) {
if (node.commentView != null) flattenedCommentNodes.add(node);

for (CommentNode child in node.replies) {
flatten(child);
}
}

flatten(root);
return flattenedCommentNodes;
}
}
64 changes: 57 additions & 7 deletions lib/comment/utils/comment.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:lemmy_api_client/v3.dart';

import 'package:thunder/comment/models/comment_node.dart';
import 'package:thunder/utils/date_time.dart';
import 'package:thunder/account/models/account.dart';
import 'package:thunder/core/singletons/lemmy_client.dart';
Expand All @@ -9,11 +10,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:thunder/utils/global_context.dart';

// Optimistically updates a comment
CommentView optimisticallyVoteComment(CommentViewTree commentViewTree, int voteType) {
int newScore = commentViewTree.commentView!.counts.score;
int newUpvotes = commentViewTree.commentView!.counts.upvotes;
int newDownvotes = commentViewTree.commentView!.counts.downvotes;
int? existingVoteType = commentViewTree.commentView!.myVote;
CommentView optimisticallyVoteComment(CommentView commentView, int voteType) {
int newScore = commentView.counts.score;
int newUpvotes = commentView.counts.upvotes;
int newDownvotes = commentView.counts.downvotes;
int? existingVoteType = commentView.myVote;

switch (voteType) {
case -1:
Expand All @@ -38,9 +39,9 @@ CommentView optimisticallyVoteComment(CommentViewTree commentViewTree, int voteT
break;
}

return commentViewTree.commentView!.copyWith(
return commentView.copyWith(
myVote: voteType,
counts: commentViewTree.commentView!.counts.copyWith(
counts: commentView.counts.copyWith(
score: newScore,
upvotes: newUpvotes,
downvotes: newDownvotes,
Expand All @@ -65,6 +66,11 @@ Future<CommentView> voteComment(int commentId, int score) async {
return updatedCommentView;
}

/// Optimistically saves a comment without sending the network request
CommentView optimisticallySaveComment(CommentView commentView, bool saved) {
return commentView.copyWith(saved: saved);
}

/// Logic to save a comment
Future<CommentView> saveComment(int commentId, bool save) async {
Account? account = await fetchActiveProfileAccount();
Expand All @@ -82,7 +88,47 @@ Future<CommentView> saveComment(int commentId, bool save) async {
return updatedCommentView;
}

/// Optimistically deletes a comment without sending the network request
CommentView optimisticallyDeleteComment(CommentView commentView, bool deleted) {
return commentView.copyWith(comment: commentView.comment.copyWith(deleted: deleted));
}

/// Logic to delete a comment
Future<CommentView> deleteComment(int commentId, bool deleted) async {
Account? account = await fetchActiveProfileAccount();
LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3;

if (account?.jwt == null) throw Exception(AppLocalizations.of(GlobalContext.context)!.userNotLoggedIn);

CommentResponse commentResponse = await lemmy.run(DeleteComment(
auth: account!.jwt!,
commentId: commentId,
deleted: deleted,
));

CommentView updatedCommentView = commentResponse.commentView;
return updatedCommentView;
}

/// Builds a tree of [CommentView] given a flattened list [CommentView].
///
/// We need to associate replies to the proper parent comment since we cannot guarantee order in the flattened list from the API.
CommentNode buildCommentTree(List<CommentView> comments, {bool flatten = false}) {
CommentNode root = CommentNode(commentView: null, replies: []);

for (CommentView commentView in comments) {
List<String> commentPath = commentView.comment.path.split('.');
String parentId = commentPath.length > 2 ? commentPath[commentPath.length - 2] : commentPath.first;

CommentNode commentNode = CommentNode(commentView: commentView, replies: []);
CommentNode.insertCommentNode(root, parentId, commentNode);
}

return root;
}

/// Builds a tree of comments given a flattened list
@Deprecated('This function is used only for the legacy PostPage. Use buildCommentTree instead.')
List<CommentViewTree> buildCommentViewTree(List<CommentView> comments, {bool flatten = false}) {
Map<String, CommentViewTree> commentMap = {};

Expand Down Expand Up @@ -119,6 +165,7 @@ List<CommentViewTree> buildCommentViewTree(List<CommentView> comments, {bool fla
return commentMap.values.where((commentView) => commentView.commentView!.comment.path.isEmpty || commentView.commentView!.comment.path == '0.${commentView.commentView!.comment.id}').toList();
}

@Deprecated('This function is used only for the legacy PostPage. Use CommentNode.insertCommentNode instead.')
List<CommentViewTree> insertNewComment(List<CommentViewTree> comments, CommentView commentView) {
List<String> parentIds = commentView.comment.path.split('.');
String commentTime = commentView.comment.published.toIso8601String();
Expand Down Expand Up @@ -146,6 +193,7 @@ List<CommentViewTree> insertNewComment(List<CommentViewTree> comments, CommentVi
return comments;
}

@Deprecated('This function is used only for the legacy PostPage. Use CommentNode.findCommentNode instead.')
CommentViewTree? findParentComment(int index, List<String> parentIds, String targetId, List<CommentViewTree> comments) {
for (CommentViewTree existing in comments) {
if (existing.commentView?.comment.id.toString() != parentIds[index]) {
Expand All @@ -162,6 +210,7 @@ CommentViewTree? findParentComment(int index, List<String> parentIds, String tar
return null;
}

@Deprecated('This function is used only for the legacy PostPage')
List<int> findCommentIndexesFromCommentViewTree(List<CommentViewTree> commentTrees, int commentId, [List<int>? indexes]) {
indexes ??= [];

Expand All @@ -185,6 +234,7 @@ List<int> findCommentIndexesFromCommentViewTree(List<CommentViewTree> commentTre
}

// Used for modifying the comment current comment tree so we don't have to refresh the whole thing
@Deprecated('This function is used only for the legacy PostPage')
bool updateModifiedComment(List<CommentViewTree> commentTrees, CommentView commentView) {
for (int i = 0; i < commentTrees.length; i++) {
if (commentTrees[i].commentView!.comment.id == commentView.comment.id) {
Expand Down
2 changes: 1 addition & 1 deletion lib/comment/utils/navigate_comment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import 'package:thunder/comment/view/create_comment_page.dart';
import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/core/models/post_view_media.dart';
import 'package:thunder/post/bloc/post_bloc.dart';
import 'package:thunder/post/pages/post_page.dart';
import 'package:thunder/post/pages/legacy_post_page.dart';
import 'package:thunder/shared/pages/loading_page.dart';
import 'package:thunder/shared/snackbar.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
Expand Down
2 changes: 0 additions & 2 deletions lib/comment/view/create_comment_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,6 @@ class _CreateCommentPageState extends State<CreateCommentPage> {
useDisplayNames: true,
postViewMedia: widget.postViewMedia!,
crossPosts: const [],
moderators: const [],
viewSource: viewSource,
onViewSourceToggled: () => setState(() => viewSource = !viewSource),
showQuickPostActionBar: false,
Expand All @@ -328,7 +327,6 @@ class _CreateCommentPageState extends State<CreateCommentPage> {
onSaveAction: (_, __) {},
onReplyEditAction: (_, __) {},
onReportAction: (_) {},
now: DateTime.now().toUtc(),
onDeleteAction: (_, __) {},
isUserLoggedIn: true,
isOwnComment: false,
Expand Down
Loading
Loading