2019-11-15 18:36:47 +01:00
|
|
|
<template>
|
2020-02-18 08:57:00 +01:00
|
|
|
<div>
|
|
|
|
<form
|
|
|
|
class="new-comment"
|
2020-10-05 17:42:53 +02:00
|
|
|
v-if="isAbleToComment"
|
2020-02-18 08:57:00 +01:00
|
|
|
@submit.prevent="createCommentForEvent(newComment)"
|
|
|
|
@keyup.ctrl.enter="createCommentForEvent(newComment)"
|
|
|
|
>
|
2020-11-30 10:24:11 +01:00
|
|
|
<b-notification
|
|
|
|
v-if="isEventOrganiser && !areCommentsClosed"
|
|
|
|
:closable="false"
|
|
|
|
>{{ $t("Comments are closed for everybody else.") }}</b-notification
|
|
|
|
>
|
2020-02-18 08:57:00 +01:00
|
|
|
<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">
|
2020-11-30 10:24:11 +01:00
|
|
|
<editor
|
|
|
|
ref="commenteditor"
|
|
|
|
mode="comment"
|
|
|
|
v-model="newComment.text"
|
|
|
|
/>
|
2020-02-18 08:57:00 +01:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
<div class="send-comment">
|
2020-11-30 10:24:11 +01:00
|
|
|
<b-button native-type="submit" type="is-primary">{{
|
|
|
|
$t("Post a comment")
|
|
|
|
}}</b-button>
|
2020-02-18 08:57:00 +01:00
|
|
|
</div>
|
2019-11-15 18:36:47 +01:00
|
|
|
</div>
|
2020-02-18 08:57:00 +01:00
|
|
|
</article>
|
|
|
|
</form>
|
2020-10-20 10:59:56 +02:00
|
|
|
<b-notification v-else :closable="false">{{
|
|
|
|
$t("The organiser has chosen to close comments.")
|
|
|
|
}}</b-notification>
|
2020-02-18 08:57:00 +01:00
|
|
|
<transition name="comment-empty-list" mode="out-in">
|
2020-11-30 10:24:11 +01:00
|
|
|
<transition-group
|
|
|
|
name="comment-list"
|
|
|
|
v-if="comments.length"
|
|
|
|
class="comment-list"
|
|
|
|
tag="ul"
|
|
|
|
>
|
2020-02-18 08:57:00 +01:00
|
|
|
<comment
|
|
|
|
class="root-comment"
|
|
|
|
:comment="comment"
|
|
|
|
:event="event"
|
|
|
|
v-for="comment in filteredOrderedComments"
|
|
|
|
:key="comment.id"
|
|
|
|
@create-comment="createCommentForEvent"
|
|
|
|
@delete-comment="deleteComment"
|
|
|
|
/>
|
|
|
|
</transition-group>
|
2020-10-20 10:59:56 +02:00
|
|
|
<div v-else-if="isAbleToComment" class="no-comments">
|
2020-02-18 08:57:00 +01:00
|
|
|
<span>{{ $t("No comments yet") }}</span>
|
|
|
|
</div>
|
|
|
|
</transition>
|
|
|
|
</div>
|
2019-11-15 18:36:47 +01:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
2020-02-18 08:57:00 +01:00
|
|
|
import { Prop, Vue, Component, Watch } from "vue-property-decorator";
|
|
|
|
import Comment from "@/components/Comment/Comment.vue";
|
|
|
|
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
|
2020-11-27 19:27:44 +01:00
|
|
|
import { CommentModeration } from "@/types/enums";
|
2020-02-18 08:57:00 +01:00
|
|
|
import { CommentModel, IComment } from "../../types/comment.model";
|
2019-11-15 18:36:47 +01:00
|
|
|
import {
|
2020-02-18 08:57:00 +01:00
|
|
|
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";
|
2020-11-06 11:34:32 +01:00
|
|
|
import { IEvent } from "../../types/event.model";
|
2019-11-15 18:36:47 +01:00
|
|
|
|
|
|
|
@Component({
|
|
|
|
apollo: {
|
|
|
|
currentActor: {
|
|
|
|
query: CURRENT_ACTOR_CLIENT,
|
|
|
|
},
|
|
|
|
comments: {
|
|
|
|
query: COMMENTS_THREADS,
|
|
|
|
variables() {
|
|
|
|
return {
|
|
|
|
eventUUID: this.event.uuid,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
update(data) {
|
2020-11-30 10:24:11 +01:00
|
|
|
return data.event.comments.map(
|
|
|
|
(comment: IComment) => new CommentModel(comment)
|
|
|
|
);
|
2019-11-15 18:36:47 +01:00
|
|
|
},
|
|
|
|
skip() {
|
|
|
|
return !this.event.uuid;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
components: {
|
|
|
|
Comment,
|
|
|
|
IdentityPickerWrapper,
|
2020-11-30 10:24:11 +01:00
|
|
|
editor: () =>
|
|
|
|
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
2019-11-15 18:36:47 +01:00
|
|
|
},
|
|
|
|
})
|
|
|
|
export default class CommentTree extends Vue {
|
|
|
|
@Prop({ required: false, type: Object }) event!: IEvent;
|
|
|
|
|
|
|
|
newComment: IComment = new CommentModel();
|
2020-02-18 08:57:00 +01:00
|
|
|
|
2019-11-15 18:36:47 +01:00
|
|
|
currentActor!: IPerson;
|
2020-02-18 08:57:00 +01:00
|
|
|
|
2019-11-15 18:36:47 +01:00
|
|
|
comments: IComment[] = [];
|
2020-02-18 08:57:00 +01:00
|
|
|
|
2019-11-15 18:36:47 +01:00
|
|
|
CommentModeration = CommentModeration;
|
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
@Watch("currentActor")
|
2020-09-29 09:53:48 +02:00
|
|
|
watchCurrentActor(currentActor: IPerson): void {
|
2019-11-15 18:36:47 +01:00
|
|
|
this.newComment.actor = currentActor;
|
|
|
|
}
|
|
|
|
|
2020-09-29 09:53:48 +02:00
|
|
|
async createCommentForEvent(comment: IComment): Promise<void> {
|
2019-11-15 18:36:47 +01:00
|
|
|
try {
|
2020-08-14 11:32:23 +02:00
|
|
|
if (!comment.actor) return;
|
2019-11-15 18:36:47 +01:00
|
|
|
await this.$apollo.mutate({
|
|
|
|
mutation: CREATE_COMMENT_FROM_EVENT,
|
|
|
|
variables: {
|
|
|
|
eventId: this.event.id,
|
|
|
|
text: comment.text,
|
2020-11-30 10:24:11 +01:00
|
|
|
inReplyToCommentId: comment.inReplyToComment
|
|
|
|
? comment.inReplyToComment.id
|
|
|
|
: null,
|
2019-11-15 18:36:47 +01:00
|
|
|
},
|
|
|
|
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;
|
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
// if it's no a root comment, we first need to find
|
|
|
|
// existing replies and add the new reply to it
|
|
|
|
if (comment.originComment !== undefined) {
|
|
|
|
const { originComment } = comment;
|
|
|
|
const parentCommentIndex = oldComments.findIndex(
|
|
|
|
(oldComment) => oldComment.id === originComment.id
|
|
|
|
);
|
2019-11-15 18:36:47 +01:00
|
|
|
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();
|
2020-10-05 17:42:53 +02:00
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
|
|
|
this.$notifier.error(error.graphQLErrors[0].message);
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-29 09:53:48 +02:00
|
|
|
async deleteComment(comment: IComment): Promise<void> {
|
|
|
|
try {
|
|
|
|
await this.$apollo.mutate({
|
|
|
|
mutation: DELETE_COMMENT,
|
|
|
|
variables: {
|
|
|
|
commentId: comment.id,
|
|
|
|
},
|
|
|
|
update: (store, { data }) => {
|
|
|
|
if (data == null) return;
|
|
|
|
const deletedCommentId = data.deleteComment.id;
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-09-29 09:53:48 +02:00
|
|
|
const commentsData = store.readQuery<{ event: IEvent }>({
|
|
|
|
query: COMMENTS_THREADS,
|
2019-11-15 18:36:47 +01:00
|
|
|
variables: {
|
2020-09-29 09:53:48 +02:00
|
|
|
eventUUID: this.event.uuid,
|
2019-11-15 18:36:47 +01:00
|
|
|
},
|
|
|
|
});
|
2020-09-29 09:53:48 +02:00
|
|
|
if (!commentsData) return;
|
|
|
|
const { event } = commentsData;
|
|
|
|
const { comments: oldComments } = event;
|
|
|
|
|
|
|
|
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;
|
2020-11-30 10:24:11 +01:00
|
|
|
const replies = oldReplyList.filter(
|
|
|
|
(reply) => reply.id !== deletedCommentId
|
|
|
|
);
|
2020-09-29 09:53:48 +02:00
|
|
|
store.writeQuery({
|
|
|
|
query: FETCH_THREAD_REPLIES,
|
|
|
|
variables: {
|
|
|
|
threadId: comment.originComment.id,
|
|
|
|
},
|
|
|
|
data: { thread: replies },
|
|
|
|
});
|
|
|
|
|
|
|
|
const { originComment } = comment;
|
|
|
|
|
|
|
|
const parentCommentIndex = oldComments.findIndex(
|
|
|
|
(oldComment) => oldComment.id === 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
|
2020-11-30 10:24:11 +01:00
|
|
|
event.comments = oldComments.filter(
|
|
|
|
(reply) => reply.id !== deletedCommentId
|
|
|
|
);
|
2020-09-29 09:53:48 +02:00
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
store.writeQuery({
|
2020-09-29 09:53:48 +02:00
|
|
|
query: COMMENTS_THREADS,
|
2019-11-15 18:36:47 +01:00
|
|
|
variables: {
|
2020-09-29 09:53:48 +02:00
|
|
|
eventUUID: this.event.uuid,
|
2019-11-15 18:36:47 +01:00
|
|
|
},
|
2020-09-29 09:53:48 +02:00
|
|
|
data: { event },
|
2019-11-15 18:36:47 +01:00
|
|
|
});
|
2020-09-29 09:53:48 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
// this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id);
|
2020-10-05 17:42:53 +02:00
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
|
|
|
this.$notifier.error(error.graphQLErrors[0].message);
|
|
|
|
}
|
2020-09-29 09:53:48 +02:00
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
get orderedComments(): IComment[] {
|
2020-02-18 08:57:00 +01:00
|
|
|
return this.comments
|
|
|
|
.filter((comment) => comment.inReplyToComment == null)
|
|
|
|
.sort((a, b) => {
|
|
|
|
if (a.updatedAt && b.updatedAt) {
|
2020-11-30 10:24:11 +01:00
|
|
|
return (
|
|
|
|
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
|
|
);
|
2020-02-18 08:57:00 +01:00
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
get filteredOrderedComments(): IComment[] {
|
2020-11-30 10:24:11 +01:00
|
|
|
return this.orderedComments.filter(
|
|
|
|
(comment) => !comment.deletedAt || comment.totalReplies > 0
|
|
|
|
);
|
2019-11-15 18:36:47 +01:00
|
|
|
}
|
2020-10-05 17:42:53 +02:00
|
|
|
|
2020-10-20 10:59:56 +02:00
|
|
|
get isEventOrganiser(): boolean {
|
|
|
|
return (
|
|
|
|
this.currentActor.id !== undefined &&
|
|
|
|
this.event.organizerActor !== undefined &&
|
|
|
|
this.currentActor.id === this.event.organizerActor.id
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
get areCommentsClosed(): boolean {
|
|
|
|
return (
|
|
|
|
this.currentActor.id !== undefined &&
|
|
|
|
this.event.options.commentModeration !== CommentModeration.CLOSED
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:42:53 +02:00
|
|
|
get isAbleToComment(): boolean {
|
|
|
|
if (this.currentActor.id) {
|
2020-10-20 10:59:56 +02:00
|
|
|
return this.areCommentsClosed || this.isEventOrganiser;
|
2020-10-05 17:42:53 +02:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2020-02-18 08:57:00 +01:00
|
|
|
form.new-comment {
|
|
|
|
padding-bottom: 1rem;
|
|
|
|
|
|
|
|
.media-content {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
align-content: center;
|
|
|
|
|
|
|
|
.field {
|
|
|
|
flex: 1;
|
|
|
|
padding-right: 10px;
|
|
|
|
margin-bottom: 0;
|
2019-11-15 18:36:47 +01:00
|
|
|
}
|
2020-02-18 08:57:00 +01:00
|
|
|
}
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
.no-comments {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
span {
|
|
|
|
text-align: center;
|
|
|
|
margin-bottom: 10px;
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
img {
|
|
|
|
max-width: 250px;
|
|
|
|
align-self: center;
|
|
|
|
}
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
ul.comment-list li {
|
|
|
|
margin-bottom: 16px;
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
.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;
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
.comment-list-enter {
|
|
|
|
opacity: 0;
|
|
|
|
transform: translateX(50px) scaleY(0.5);
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
.comment-list-enter-to {
|
|
|
|
opacity: 1;
|
|
|
|
transform: translateX(0) scaleY(1);
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
.comment-list-leave-active,
|
|
|
|
.comment-empty-list-active {
|
|
|
|
position: absolute;
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
.comment-list-leave-to,
|
|
|
|
.comment-empty-list-leave-to {
|
|
|
|
opacity: 0;
|
|
|
|
transform: scaleY(0);
|
|
|
|
transform-origin: center top;
|
|
|
|
}
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
/*.comment-empty-list-enter-active {*/
|
|
|
|
/* transition: opacity .5s;*/
|
|
|
|
/*}*/
|
2019-11-15 18:36:47 +01:00
|
|
|
|
2020-02-18 08:57:00 +01:00
|
|
|
/*.comment-empty-list-enter {*/
|
|
|
|
/* opacity: 0;*/
|
|
|
|
/*}*/
|
2019-11-15 18:36:47 +01:00
|
|
|
</style>
|