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