Comment fixes

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-05-17 11:36:28 +02:00
parent b5a5de5c0c
commit 679600f003
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
7 changed files with 88 additions and 113 deletions

View File

@ -60,11 +60,6 @@ export const typePolicies: TypePolicies = {
relatedEvents: pageLimitPagination<IEvent>(), relatedEvents: pageLimitPagination<IEvent>(),
}, },
}, },
Comment: {
fields: {
replies: pageLimitPagination<IComment>(),
},
},
RootQueryType: { RootQueryType: {
fields: { fields: {
relayFollowers: paginatedLimitPagination<IFollower>(), relayFollowers: paginatedLimitPagination<IFollower>(),

View File

@ -55,7 +55,7 @@
<span class="icons" v-if="!comment.deletedAt"> <span class="icons" v-if="!comment.deletedAt">
<button <button
v-if="comment.actor.id === currentActor.id" v-if="comment.actor.id === currentActor.id"
@click="$emit('delete-comment', comment)" @click="deleteComment"
> >
<b-icon icon="delete" size="is-small" aria-hidden="true" /> <b-icon icon="delete" size="is-small" aria-hidden="true" />
<span class="visually-hidden">{{ $t("Delete") }}</span> <span class="visually-hidden">{{ $t("Delete") }}</span>
@ -183,7 +183,6 @@ import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model"; import { CommentModel, IComment } from "../../types/comment.model";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { IPerson, usernameWithDomain } from "../../types/actor"; import { IPerson, usernameWithDomain } from "../../types/actor";
import { COMMENTS_THREADS, FETCH_THREAD_REPLIES } from "../../graphql/comment";
import { IEvent } from "../../types/event.model"; import { IEvent } from "../../types/event.model";
import ReportModal from "../Report/ReportModal.vue"; import ReportModal from "../Report/ReportModal.vue";
import { IReport } from "../../types/report.model"; import { IReport } from "../../types/report.model";
@ -257,39 +256,15 @@ export default class Comment extends Vue {
this.$emit("create-comment", this.newComment); this.$emit("create-comment", this.newComment);
this.newComment = new CommentModel(); this.newComment = new CommentModel();
this.replyTo = false; this.replyTo = false;
this.showReplies = true;
} }
async fetchReplies(): Promise<void> { deleteComment(): void {
const parentId = this.comment.id; this.$emit("delete-comment", this.comment);
const { data } = await this.$apollo.query<{ thread: IComment[] }>({ this.showReplies = false;
query: FETCH_THREAD_REPLIES, }
variables: {
threadId: parentId, fetchReplies(): void {
},
});
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 },
});
this.showReplies = true; this.showReplies = true;
} }

View File

@ -82,8 +82,7 @@ import { CommentModel, IComment } from "../../types/comment.model";
import { import {
CREATE_COMMENT_FROM_EVENT, CREATE_COMMENT_FROM_EVENT,
DELETE_COMMENT, DELETE_COMMENT,
COMMENTS_THREADS, COMMENTS_THREADS_WITH_REPLIES,
FETCH_THREAD_REPLIES,
} from "../../graphql/comment"; } from "../../graphql/comment";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { IPerson } from "../../types/actor"; import { IPerson } from "../../types/actor";
@ -96,7 +95,7 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
query: CURRENT_ACTOR_CLIENT, query: CURRENT_ACTOR_CLIENT,
}, },
comments: { comments: {
query: COMMENTS_THREADS, query: COMMENTS_THREADS_WITH_REPLIES,
variables() { variables() {
return { return {
eventUUID: this.event.uuid, eventUUID: this.event.uuid,
@ -145,6 +144,7 @@ export default class CommentTree extends Vue {
} }
async createCommentForEvent(comment: IComment): Promise<void> { async createCommentForEvent(comment: IComment): Promise<void> {
console.log("creating comment", comment);
this.emptyCommentError = ["", "<p></p>"].includes(comment.text); this.emptyCommentError = ["", "<p></p>"].includes(comment.text);
if (this.emptyCommentError) return; if (this.emptyCommentError) return;
try { try {
@ -160,21 +160,19 @@ export default class CommentTree extends Vue {
}, },
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => { update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
if (data == null) return; if (data == null) return;
const newComment = data.createComment;
// comments are attached to the event, so we can pass it to replies later // 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 // we load all existing threads
const commentThreadsData = store.readQuery<{ event: IEvent }>({ const commentThreadsData = store.readQuery<{ event: IEvent }>({
query: COMMENTS_THREADS, query: COMMENTS_THREADS_WITH_REPLIES,
variables: { variables: {
eventUUID: this.event.uuid, eventUUID: this.event.uuid,
}, },
}); });
if (!commentThreadsData) return; if (!commentThreadsData) return;
const { event } = commentThreadsData; const { event } = commentThreadsData;
const { comments: oldComments } = event; const oldComments = [...event.comments];
// if it's no a root comment, we first need to find // if it's no a root comment, we first need to find
// existing replies and add the new reply to it // existing replies and add the new reply to it
@ -185,44 +183,25 @@ export default class CommentTree extends Vue {
); );
const parentComment = oldComments[parentCommentIndex]; 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 // replace the root comment with has the updated list of replies in the thread list
parentComment.replies = oldReplyList; oldComments.splice(parentCommentIndex, 1, {
event.comments.splice(parentCommentIndex, 1, parentComment); ...parentComment,
} replies: [...parentComment.replies, newComment],
});
} else { } else {
// otherwise it's simply a new thread and we add it to the list // otherwise it's simply a new thread and we add it to the list
oldComments.push(newComment); oldComments.push(newComment);
} }
// finally we save the thread list // finally we save the thread list
event.comments = oldComments;
store.writeQuery({ store.writeQuery({
query: COMMENTS_THREADS, query: COMMENTS_THREADS_WITH_REPLIES,
data: { event }, data: {
event: {
...event,
comments: oldComments,
},
},
variables: { variables: {
eventUUID: this.event.uuid, eventUUID: this.event.uuid,
}, },
@ -255,58 +234,61 @@ export default class CommentTree extends Vue {
const deletedCommentId = data.deleteComment.id; const deletedCommentId = data.deleteComment.id;
const commentsData = store.readQuery<{ event: IEvent }>({ const commentsData = store.readQuery<{ event: IEvent }>({
query: COMMENTS_THREADS, query: COMMENTS_THREADS_WITH_REPLIES,
variables: { variables: {
eventUUID: this.event.uuid, eventUUID: this.event.uuid,
}, },
}); });
if (!commentsData) return; if (!commentsData) return;
const { event } = commentsData; const { event } = commentsData;
const { comments: oldComments } = event; let updatedComments: IComment[] = [...event.comments];
if (comment.originComment) { if (comment.originComment) {
// we have deleted a reply to a thread // 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 { originComment } = comment;
const parentCommentIndex = oldComments.findIndex( const parentCommentIndex = updatedComments.findIndex(
(oldComment) => oldComment.id === originComment.id (oldComment) => oldComment.id === originComment.id
); );
const parentComment = oldComments[parentCommentIndex]; const parentComment = updatedComments[parentCommentIndex];
parentComment.replies = replies; const updatedReplies = parentComment.replies.map((reply) => {
parentComment.totalReplies -= 1; if (reply.id === deletedCommentId) {
oldComments.splice(parentCommentIndex, 1, parentComment); return {
event.comments = oldComments; ...reply,
deletedAt: new Date().toString(),
};
}
return reply;
});
updatedComments.splice(parentCommentIndex, 1, {
...parentComment,
replies: updatedReplies,
totalReplies: parentComment.totalReplies - 1,
});
console.log("updatedComments", updatedComments);
} else { } else {
// we have deleted a thread itself // we have deleted a thread itself
event.comments = oldComments.filter( updatedComments = updatedComments.map((reply) => {
(reply) => reply.id !== deletedCommentId if (reply.id === deletedCommentId) {
); return {
...reply,
deletedAt: new Date().toString(),
};
}
return reply;
});
} }
store.writeQuery({ store.writeQuery({
query: COMMENTS_THREADS, query: COMMENTS_THREADS_WITH_REPLIES,
variables: { variables: {
eventUUID: this.event.uuid, eventUUID: this.event.uuid,
}, },
data: { event }, data: {
event: {
...event,
comments: updatedComments,
},
},
}); });
}, },
}); });

View File

@ -38,6 +38,12 @@ export const COMMENT_RECURSIVE_FRAGMENT = gql`
} }
replies { replies {
...CommentFields ...CommentFields
inReplyToComment {
...CommentFields
}
originComment {
...CommentFields
}
replies { replies {
...CommentFields ...CommentFields
} }
@ -68,6 +74,19 @@ export const COMMENTS_THREADS = gql`
${COMMENT_FIELDS_FRAGMENT} ${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` export const CREATE_COMMENT_FROM_EVENT = gql`
mutation CreateCommentFromEvent( mutation CreateCommentFromEvent(
$eventId: ID! $eventId: ID!

View File

@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
use Absinthe.Schema.Notation 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.{Actors, Addresses, Discussions}
alias Mobilizon.GraphQL.Resolvers.{Event, Media, Tag} 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 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 end
# field(:tracks, list_of(:track)) # field(:tracks, list_of(:track))

View File

@ -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(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id)
belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id) belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id)
belongs_to(:discussion, Discussion, type: :binary_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) many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
has_many(:mentions, Mention) has_many(:mentions, Mention)
many_to_many(:media, Media, join_through: "comments_medias", on_replace: :delete) many_to_many(:media, Media, join_through: "comments_medias", on_replace: :delete)

View File

@ -72,7 +72,7 @@ defmodule Mobilizon.Discussions do
Read: https://hexdocs.pm/absinthe/ecto.html#dataloader Read: https://hexdocs.pm/absinthe/ecto.html#dataloader
""" """
@spec query(atom(), map()) :: Ecto.Queryable.t() @spec query(atom(), map()) :: Ecto.Queryable.t()
def query(Comment, _params) do def query(Comment, %{top_level: true}) do
Comment Comment
|> join(:left, [c], r in Comment, on: r.origin_comment_id == c.id) |> join(:left, [c], r in Comment, on: r.origin_comment_id == c.id)
|> where([c, _], is_nil(c.in_reply_to_comment_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)}) |> select([c, r], %{c | total_replies: count(r.id)})
end end
def query(Comment, _) do
order_by(Comment, [c], asc: :published_at)
end
def query(queryable, _) do def query(queryable, _) do
queryable queryable
end end