Introduce comments below events
Also add tomstones Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
328
js/src/components/Comment/CommentTree.vue
Normal file
328
js/src/components/Comment/CommentTree.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds-desktop">
|
||||
<form class="new-comment" v-if="currentActor.id && event.options.commentModeration !== CommentModeration.CLOSED" @submit.prevent="createCommentForEvent(newComment)" @keyup.ctrl.enter="createCommentForEvent(newComment)">
|
||||
<article class="media">
|
||||
<figure class="media-left">
|
||||
<identity-picker-wrapper :inline="false" v-model="newComment.actor" />
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="field">
|
||||
<p class="control">
|
||||
<editor ref="commenteditor" mode="comment" v-model="newComment.text" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="send-comment">
|
||||
<b-button native-type="submit" type="is-info">{{ $t('Post a comment') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</form>
|
||||
<b-notification v-else-if="event.options.commentModeration === CommentModeration.CLOSED" :closable="false">
|
||||
{{ $t('Comments have been closed.') }}
|
||||
</b-notification>
|
||||
<transition name="comment-empty-list" mode="out-in">
|
||||
<transition-group name="comment-list" v-if="comments.length" class="comment-list" tag="ul">
|
||||
<comment
|
||||
class="root-comment"
|
||||
:comment="comment"
|
||||
:event="event"
|
||||
v-for="comment in orderedComments"
|
||||
v-if="!comment.deletedAt || comment.totalReplies > 0"
|
||||
:key="comment.id"
|
||||
@create-comment="createCommentForEvent"
|
||||
@delete-comment="deleteComment"
|
||||
/>
|
||||
</transition-group>
|
||||
<div v-else class="no-comments">
|
||||
<span>{{ $t('No comments yet') }}</span>
|
||||
<img src="../../assets/undraw_just_saying.svg" alt="" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Prop, Vue, Component, Watch } from 'vue-property-decorator';
|
||||
import { CommentModel, IComment } from '@/types/comment.model';
|
||||
import {
|
||||
CREATE_COMMENT_FROM_EVENT,
|
||||
DELETE_COMMENT, COMMENTS_THREADS, FETCH_THREAD_REPLIES,
|
||||
} from '@/graphql/comment';
|
||||
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { IPerson } from '@/types/actor';
|
||||
import Comment from '@/components/Comment/Comment.vue';
|
||||
import { IEvent, CommentModeration } from '@/types/event.model';
|
||||
import IdentityPickerWrapper from '@/views/Account/IdentityPickerWrapper.vue';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
currentActor: {
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
},
|
||||
comments: {
|
||||
query: COMMENTS_THREADS,
|
||||
variables() {
|
||||
return {
|
||||
eventUUID: this.event.uuid,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.event.comments.map((comment) => new CommentModel(comment));
|
||||
},
|
||||
skip() {
|
||||
return !this.event.uuid;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Comment,
|
||||
IdentityPickerWrapper,
|
||||
editor: () => import(/* webpackChunkName: "editor" */ '@/components/Editor.vue'),
|
||||
},
|
||||
})
|
||||
export default class CommentTree extends Vue {
|
||||
@Prop({ required: false, type: Object }) event!: IEvent;
|
||||
|
||||
newComment: IComment = new CommentModel();
|
||||
currentActor!: IPerson;
|
||||
comments: IComment[] = [];
|
||||
CommentModeration = CommentModeration;
|
||||
|
||||
@Watch('currentActor')
|
||||
watchCurrentActor(currentActor: IPerson) {
|
||||
this.newComment.actor = currentActor;
|
||||
}
|
||||
|
||||
async createCommentForEvent(comment: IComment) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: CREATE_COMMENT_FROM_EVENT,
|
||||
variables: {
|
||||
eventId: this.event.id,
|
||||
actorId: comment.actor.id,
|
||||
text: comment.text,
|
||||
inReplyToCommentId: comment.inReplyToComment ? comment.inReplyToComment.id : null,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
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;
|
||||
|
||||
// we load all existing threads
|
||||
const commentThreadsData = store.readQuery<{ event: IEvent }>({
|
||||
query: COMMENTS_THREADS,
|
||||
variables: {
|
||||
eventUUID: this.event.uuid,
|
||||
},
|
||||
});
|
||||
if (!commentThreadsData) return;
|
||||
const { event } = commentThreadsData;
|
||||
const { comments: oldComments } = event;
|
||||
|
||||
// if it's no a root comment, we first need to find existing replies and add the new reply to it
|
||||
if (comment.originComment) {
|
||||
// @ts-ignore
|
||||
const parentCommentIndex = oldComments.findIndex(oldComment => oldComment.id === comment.originComment.id);
|
||||
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);
|
||||
}
|
||||
} 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 },
|
||||
variables: {
|
||||
eventUUID: this.event.uuid,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// and reset the new comment field
|
||||
this.newComment = new CommentModel();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteComment(comment: IComment) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_COMMENT,
|
||||
variables: {
|
||||
commentId: comment.id,
|
||||
actorId: this.currentActor.id,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
const deletedCommentId = data.deleteComment.id;
|
||||
|
||||
const commentsData = store.readQuery<{ event: IEvent }>({
|
||||
query: COMMENTS_THREADS,
|
||||
variables: {
|
||||
eventUUID: this.event.uuid,
|
||||
},
|
||||
});
|
||||
if (!commentsData) return;
|
||||
const { event } = commentsData;
|
||||
const { comments: oldComments } = event;
|
||||
|
||||
if (comment.originComment) {
|
||||
// we have deleted a reply to a thread
|
||||
const data = store.readQuery<{ thread: IComment[] }>({
|
||||
query: FETCH_THREAD_REPLIES,
|
||||
variables: {
|
||||
threadId: comment.originComment.id,
|
||||
},
|
||||
});
|
||||
if (!data) return;
|
||||
const { thread: oldReplyList } = data;
|
||||
const replies = oldReplyList.filter(reply => reply.id !== deletedCommentId);
|
||||
store.writeQuery({
|
||||
query: FETCH_THREAD_REPLIES,
|
||||
variables: {
|
||||
threadId: comment.originComment.id,
|
||||
},
|
||||
data: { thread: replies },
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const parentCommentIndex = oldComments.findIndex(oldComment => oldComment.id === comment.originComment.id);
|
||||
const parentComment = oldComments[parentCommentIndex];
|
||||
parentComment.replies = replies;
|
||||
parentComment.totalReplies -= 1;
|
||||
oldComments.splice(parentCommentIndex, 1, parentComment);
|
||||
event.comments = oldComments;
|
||||
} else {
|
||||
// we have deleted a thread itself
|
||||
event.comments = oldComments.filter(reply => reply.id !== deletedCommentId);
|
||||
}
|
||||
store.writeQuery({
|
||||
query: COMMENTS_THREADS,
|
||||
variables: {
|
||||
eventUUID: this.event.uuid,
|
||||
},
|
||||
data: { event },
|
||||
});
|
||||
},
|
||||
});
|
||||
// this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id);
|
||||
}
|
||||
|
||||
get orderedComments(): IComment[] {
|
||||
return this.comments.filter((comment => comment.inReplyToComment == null)).sort((a, b) => {
|
||||
if (a.updatedAt && b.updatedAt) {
|
||||
return (new Date(b.updatedAt)).getTime() - (new Date(a.updatedAt)).getTime();
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.new-comment {
|
||||
.media-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
|
||||
.field {
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 250px;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
ul.comment-list li {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.comment-list-enter-active,
|
||||
.comment-list-leave-active,
|
||||
.comment-list-move {
|
||||
transition: 500ms cubic-bezier(0.59, 0.12, 0.34, 0.95);
|
||||
transition-property: opacity, transform;
|
||||
}
|
||||
|
||||
.comment-list-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(50px) scaleY(0.5);
|
||||
}
|
||||
|
||||
.comment-list-enter-to {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scaleY(1);
|
||||
}
|
||||
|
||||
.comment-list-leave-active,
|
||||
.comment-empty-list-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.comment-list-leave-to,
|
||||
.comment-empty-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
/*.comment-empty-list-enter-active {*/
|
||||
/* transition: opacity .5s;*/
|
||||
/*}*/
|
||||
|
||||
/*.comment-empty-list-enter {*/
|
||||
/* opacity: 0;*/
|
||||
/*}*/
|
||||
</style>
|
||||
Reference in New Issue
Block a user