From 679600f0037b860c4b5fcfd8b13fd489f6eced71 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Mon, 17 May 2021 11:36:28 +0200
Subject: [PATCH] Comment fixes

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 js/src/apollo/utils.ts                    |   5 -
 js/src/components/Comment/Comment.vue     |  41 ++-----
 js/src/components/Comment/CommentTree.vue | 124 +++++++++-------------
 js/src/graphql/comment.ts                 |  19 ++++
 lib/graphql/schema/event.ex               |   4 +-
 lib/mobilizon/discussions/comment.ex      |   2 +-
 lib/mobilizon/discussions/discussions.ex  |   6 +-
 7 files changed, 88 insertions(+), 113 deletions(-)

diff --git a/js/src/apollo/utils.ts b/js/src/apollo/utils.ts
index 7486c0961..07ca889a3 100644
--- a/js/src/apollo/utils.ts
+++ b/js/src/apollo/utils.ts
@@ -60,11 +60,6 @@ export const typePolicies: TypePolicies = {
       relatedEvents: pageLimitPagination<IEvent>(),
     },
   },
-  Comment: {
-    fields: {
-      replies: pageLimitPagination<IComment>(),
-    },
-  },
   RootQueryType: {
     fields: {
       relayFollowers: paginatedLimitPagination<IFollower>(),
diff --git a/js/src/components/Comment/Comment.vue b/js/src/components/Comment/Comment.vue
index 090d398d1..63cfd50aa 100644
--- a/js/src/components/Comment/Comment.vue
+++ b/js/src/components/Comment/Comment.vue
@@ -55,7 +55,7 @@
           <span class="icons" v-if="!comment.deletedAt">
             <button
               v-if="comment.actor.id === currentActor.id"
-              @click="$emit('delete-comment', comment)"
+              @click="deleteComment"
             >
               <b-icon icon="delete" size="is-small" aria-hidden="true" />
               <span class="visually-hidden">{{ $t("Delete") }}</span>
@@ -183,7 +183,6 @@ import { CommentModeration } from "@/types/enums";
 import { CommentModel, IComment } from "../../types/comment.model";
 import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
 import { IPerson, usernameWithDomain } from "../../types/actor";
-import { COMMENTS_THREADS, FETCH_THREAD_REPLIES } from "../../graphql/comment";
 import { IEvent } from "../../types/event.model";
 import ReportModal from "../Report/ReportModal.vue";
 import { IReport } from "../../types/report.model";
@@ -257,39 +256,15 @@ export default class Comment extends Vue {
     this.$emit("create-comment", this.newComment);
     this.newComment = new CommentModel();
     this.replyTo = false;
+    this.showReplies = true;
   }
 
-  async fetchReplies(): Promise<void> {
-    const parentId = this.comment.id;
-    const { data } = await this.$apollo.query<{ thread: IComment[] }>({
-      query: FETCH_THREAD_REPLIES,
-      variables: {
-        threadId: parentId,
-      },
-    });
-    if (!data) return;
-    const { thread } = data;
-    const eventData = this.$apollo.getClient().readQuery<{ event: IEvent }>({
-      query: COMMENTS_THREADS,
-      variables: {
-        eventUUID: this.event.uuid,
-      },
-    });
-    if (!eventData) return;
-    const { event } = eventData;
-    const { comments } = event;
-    const parentCommentIndex = comments.findIndex(
-      (oldComment: IComment) => oldComment.id === parentId
-    );
-    const parentComment = comments[parentCommentIndex];
-    if (!parentComment) return;
-    parentComment.replies = thread;
-    comments[parentCommentIndex] = parentComment;
-    event.comments = comments;
-    this.$apollo.getClient().writeQuery({
-      query: COMMENTS_THREADS,
-      data: { event },
-    });
+  deleteComment(): void {
+    this.$emit("delete-comment", this.comment);
+    this.showReplies = false;
+  }
+
+  fetchReplies(): void {
     this.showReplies = true;
   }
 
diff --git a/js/src/components/Comment/CommentTree.vue b/js/src/components/Comment/CommentTree.vue
index 3d2f23abd..01fca3375 100644
--- a/js/src/components/Comment/CommentTree.vue
+++ b/js/src/components/Comment/CommentTree.vue
@@ -82,8 +82,7 @@ import { CommentModel, IComment } from "../../types/comment.model";
 import {
   CREATE_COMMENT_FROM_EVENT,
   DELETE_COMMENT,
-  COMMENTS_THREADS,
-  FETCH_THREAD_REPLIES,
+  COMMENTS_THREADS_WITH_REPLIES,
 } from "../../graphql/comment";
 import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
 import { IPerson } from "../../types/actor";
@@ -96,7 +95,7 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
       query: CURRENT_ACTOR_CLIENT,
     },
     comments: {
-      query: COMMENTS_THREADS,
+      query: COMMENTS_THREADS_WITH_REPLIES,
       variables() {
         return {
           eventUUID: this.event.uuid,
@@ -145,6 +144,7 @@ export default class CommentTree extends Vue {
   }
 
   async createCommentForEvent(comment: IComment): Promise<void> {
+    console.log("creating comment", comment);
     this.emptyCommentError = ["", "<p></p>"].includes(comment.text);
     if (this.emptyCommentError) return;
     try {
@@ -160,21 +160,19 @@ export default class CommentTree extends Vue {
         },
         update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
           if (data == null) return;
-          const newComment = data.createComment;
-
           // comments are attached to the event, so we can pass it to replies later
-          newComment.event = this.event;
+          const newComment = { ...data.createComment, event: this.event };
 
           // we load all existing threads
           const commentThreadsData = store.readQuery<{ event: IEvent }>({
-            query: COMMENTS_THREADS,
+            query: COMMENTS_THREADS_WITH_REPLIES,
             variables: {
               eventUUID: this.event.uuid,
             },
           });
           if (!commentThreadsData) return;
           const { event } = commentThreadsData;
-          const { comments: oldComments } = event;
+          const oldComments = [...event.comments];
 
           // if it's no a root comment, we first need to find
           // existing replies and add the new reply to it
@@ -185,44 +183,25 @@ export default class CommentTree extends Vue {
             );
             const parentComment = oldComments[parentCommentIndex];
 
-            let oldReplyList: IComment[] = [];
-            try {
-              const threadData = store.readQuery<{ thread: IComment[] }>({
-                query: FETCH_THREAD_REPLIES,
-                variables: {
-                  threadId: parentComment.id,
-                },
-              });
-              if (!threadData) return;
-              oldReplyList = threadData.thread;
-            } catch (e) {
-              // This simply means there's no loaded replies yet
-            } finally {
-              oldReplyList.push(newComment);
-
-              // save the updated list of replies (with the one we've just added)
-              store.writeQuery({
-                query: FETCH_THREAD_REPLIES,
-                data: { thread: oldReplyList },
-                variables: {
-                  threadId: parentComment.id,
-                },
-              });
-
-              // replace the root comment with has the updated list of replies in the thread list
-              parentComment.replies = oldReplyList;
-              event.comments.splice(parentCommentIndex, 1, parentComment);
-            }
+            // replace the root comment with has the updated list of replies in the thread list
+            oldComments.splice(parentCommentIndex, 1, {
+              ...parentComment,
+              replies: [...parentComment.replies, newComment],
+            });
           } else {
             // otherwise it's simply a new thread and we add it to the list
             oldComments.push(newComment);
           }
 
           // finally we save the thread list
-          event.comments = oldComments;
           store.writeQuery({
-            query: COMMENTS_THREADS,
-            data: { event },
+            query: COMMENTS_THREADS_WITH_REPLIES,
+            data: {
+              event: {
+                ...event,
+                comments: oldComments,
+              },
+            },
             variables: {
               eventUUID: this.event.uuid,
             },
@@ -255,58 +234,61 @@ export default class CommentTree extends Vue {
           const deletedCommentId = data.deleteComment.id;
 
           const commentsData = store.readQuery<{ event: IEvent }>({
-            query: COMMENTS_THREADS,
+            query: COMMENTS_THREADS_WITH_REPLIES,
             variables: {
               eventUUID: this.event.uuid,
             },
           });
           if (!commentsData) return;
           const { event } = commentsData;
-          const { comments: oldComments } = event;
+          let updatedComments: IComment[] = [...event.comments];
 
           if (comment.originComment) {
             // we have deleted a reply to a thread
-            const localData = store.readQuery<{ thread: IComment[] }>({
-              query: FETCH_THREAD_REPLIES,
-              variables: {
-                threadId: comment.originComment.id,
-              },
-            });
-            if (!localData) return;
-            const { thread: oldReplyList } = localData;
-            const replies = oldReplyList.filter(
-              (reply) => reply.id !== deletedCommentId
-            );
-            store.writeQuery({
-              query: FETCH_THREAD_REPLIES,
-              variables: {
-                threadId: comment.originComment.id,
-              },
-              data: { thread: replies },
-            });
-
             const { originComment } = comment;
 
-            const parentCommentIndex = oldComments.findIndex(
+            const parentCommentIndex = updatedComments.findIndex(
               (oldComment) => oldComment.id === originComment.id
             );
-            const parentComment = oldComments[parentCommentIndex];
-            parentComment.replies = replies;
-            parentComment.totalReplies -= 1;
-            oldComments.splice(parentCommentIndex, 1, parentComment);
-            event.comments = oldComments;
+            const parentComment = updatedComments[parentCommentIndex];
+            const updatedReplies = parentComment.replies.map((reply) => {
+              if (reply.id === deletedCommentId) {
+                return {
+                  ...reply,
+                  deletedAt: new Date().toString(),
+                };
+              }
+              return reply;
+            });
+            updatedComments.splice(parentCommentIndex, 1, {
+              ...parentComment,
+              replies: updatedReplies,
+              totalReplies: parentComment.totalReplies - 1,
+            });
+            console.log("updatedComments", updatedComments);
           } else {
             // we have deleted a thread itself
-            event.comments = oldComments.filter(
-              (reply) => reply.id !== deletedCommentId
-            );
+            updatedComments = updatedComments.map((reply) => {
+              if (reply.id === deletedCommentId) {
+                return {
+                  ...reply,
+                  deletedAt: new Date().toString(),
+                };
+              }
+              return reply;
+            });
           }
           store.writeQuery({
-            query: COMMENTS_THREADS,
+            query: COMMENTS_THREADS_WITH_REPLIES,
             variables: {
               eventUUID: this.event.uuid,
             },
-            data: { event },
+            data: {
+              event: {
+                ...event,
+                comments: updatedComments,
+              },
+            },
           });
         },
       });
diff --git a/js/src/graphql/comment.ts b/js/src/graphql/comment.ts
index 8ef20812c..cf3f5da1a 100644
--- a/js/src/graphql/comment.ts
+++ b/js/src/graphql/comment.ts
@@ -38,6 +38,12 @@ export const COMMENT_RECURSIVE_FRAGMENT = gql`
     }
     replies {
       ...CommentFields
+      inReplyToComment {
+        ...CommentFields
+      }
+      originComment {
+        ...CommentFields
+      }
       replies {
         ...CommentFields
       }
@@ -68,6 +74,19 @@ export const COMMENTS_THREADS = gql`
   ${COMMENT_FIELDS_FRAGMENT}
 `;
 
+export const COMMENTS_THREADS_WITH_REPLIES = gql`
+  query($eventUUID: UUID!) {
+    event(uuid: $eventUUID) {
+      id
+      uuid
+      comments {
+        ...CommentRecursive
+      }
+    }
+  }
+  ${COMMENT_RECURSIVE_FRAGMENT}
+`;
+
 export const CREATE_COMMENT_FROM_EVENT = gql`
   mutation CreateCommentFromEvent(
     $eventId: ID!
diff --git a/lib/graphql/schema/event.ex b/lib/graphql/schema/event.ex
index cf42ff663..e5a4446ad 100644
--- a/lib/graphql/schema/event.ex
+++ b/lib/graphql/schema/event.ex
@@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
 
   use Absinthe.Schema.Notation
 
-  import Absinthe.Resolution.Helpers, only: [dataloader: 1]
+  import Absinthe.Resolution.Helpers, only: [dataloader: 1, dataloader: 2]
 
   alias Mobilizon.{Actors, Addresses, Discussions}
   alias Mobilizon.GraphQL.Resolvers.{Event, Media, Tag}
@@ -94,7 +94,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
     )
 
     field(:comments, list_of(:comment), description: "The comments in reply to the event") do
-      resolve(dataloader(Discussions))
+      resolve(dataloader(Discussions, args: %{top_level: true}))
     end
 
     # field(:tracks, list_of(:track))
diff --git a/lib/mobilizon/discussions/comment.ex b/lib/mobilizon/discussions/comment.ex
index 8ec1bd75a..391ffb4f5 100644
--- a/lib/mobilizon/discussions/comment.ex
+++ b/lib/mobilizon/discussions/comment.ex
@@ -65,7 +65,7 @@ defmodule Mobilizon.Discussions.Comment do
     belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id)
     belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id)
     belongs_to(:discussion, Discussion, type: :binary_id)
-    has_many(:replies, Comment, foreign_key: :in_reply_to_comment_id)
+    has_many(:replies, Comment, foreign_key: :origin_comment_id)
     many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
     has_many(:mentions, Mention)
     many_to_many(:media, Media, join_through: "comments_medias", on_replace: :delete)
diff --git a/lib/mobilizon/discussions/discussions.ex b/lib/mobilizon/discussions/discussions.ex
index e951d4604..7710ea3df 100644
--- a/lib/mobilizon/discussions/discussions.ex
+++ b/lib/mobilizon/discussions/discussions.ex
@@ -72,7 +72,7 @@ defmodule Mobilizon.Discussions do
   Read: https://hexdocs.pm/absinthe/ecto.html#dataloader
   """
   @spec query(atom(), map()) :: Ecto.Queryable.t()
-  def query(Comment, _params) do
+  def query(Comment, %{top_level: true}) do
     Comment
     |> join(:left, [c], r in Comment, on: r.origin_comment_id == c.id)
     |> where([c, _], is_nil(c.in_reply_to_comment_id))
@@ -83,6 +83,10 @@ defmodule Mobilizon.Discussions do
     |> select([c, r], %{c | total_replies: count(r.id)})
   end
 
+  def query(Comment, _) do
+    order_by(Comment, [c], asc: :published_at)
+  end
+
   def query(queryable, _) do
     queryable
   end