build: switch from yarn to npm to manage js dependencies and move js contents to root

yarn v1 is being deprecated and starts to have some issues

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2023-11-14 17:24:42 +01:00
parent 32055122c3
commit 2e72f6faf4
595 changed files with 12078 additions and 7843 deletions

View File

@@ -0,0 +1,474 @@
<template>
<div>
<form
v-if="isAbleToComment"
@submit.prevent="createCommentForEvent(newCommentValue)"
class="mt-2"
>
<o-notification
v-if="isEventOrganiser && !areCommentsClosed"
:closable="false"
class="my-2"
>{{ t("Comments are closed for everybody else.") }}</o-notification
>
<article class="flex flex-wrap items-start gap-2">
<figure class="" v-if="newCommentValue.actor">
<identity-picker-wrapper
:inline="false"
v-model="newCommentValue.actor"
/>
</figure>
<div class="flex-1">
<div class="flex flex-col gap-2">
<div class="editor-wrapper">
<Editor
ref="commenteditor"
v-if="currentActor"
:currentActor="currentActor"
mode="comment"
v-model="newCommentValue.text"
:aria-label="t('Comment body')"
@submit="createCommentForEvent(newCommentValue)"
:placeholder="t('Write a new comment')"
/>
<p class="" v-if="emptyCommentError">
{{ t("Comment text can't be empty") }}
</p>
</div>
<div class="" v-if="isEventOrganiser">
<o-switch
aria-labelledby="notify-participants-toggle"
v-model="newCommentValue.isAnnouncement"
>{{ t("Notify participants") }}</o-switch
>
</div>
</div>
</div>
<div class="">
<o-button native-type="submit" variant="primary" icon-left="send">{{
t("Send")
}}</o-button>
</div>
</article>
</form>
<o-notification v-else-if="isConnected" :closable="false">{{
t("The organiser has chosen to close comments.")
}}</o-notification>
<p v-if="commentsLoading" class="text-center">
{{ t("Loading comments") }}
</p>
<transition-group tag="div" name="comment-empty-list" v-else class="mt-2">
<transition-group
key="list"
name="comment-list"
v-if="filteredOrderedComments.length && currentActor"
class="comment-list"
tag="ul"
>
<event-comment
class="root-comment my-2"
:comment="comment"
:event="event"
:currentActor="currentActor"
v-for="comment in filteredOrderedComments"
:key="comment.id"
@create-comment="createCommentForEvent"
@delete-comment="
(commentToDelete) =>
deleteComment({
commentId: commentToDelete.id as string,
originCommentId: commentToDelete.originComment?.id,
})
"
/>
</transition-group>
<empty-content v-else icon="comment" key="no-comments" :inline="true">
<span>{{ t("No comments yet") }}</span>
</empty-content>
</transition-group>
</div>
</template>
<script lang="ts" setup>
import EventComment from "@/components/Comment/EventComment.vue";
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model";
import {
CREATE_COMMENT_FROM_EVENT,
DELETE_COMMENT,
COMMENTS_THREADS_WITH_REPLIES,
} from "../../graphql/comment";
import { IEvent } from "../../types/event.model";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { useMutation, useQuery } from "@vue/apollo-composable";
import { computed, defineAsyncComponent, inject, ref, watch } from "vue";
import { IPerson } from "@/types/actor";
import { AbsintheGraphQLError } from "@/types/errors.model";
import { useI18n } from "vue-i18n";
import { Notifier } from "@/plugins/notifier";
const { currentActor } = useCurrentActorClient();
const { result: commentsResult, loading: commentsLoading } = useQuery<{
event: Pick<IEvent, "id" | "uuid" | "comments">;
}>(
COMMENTS_THREADS_WITH_REPLIES,
() => ({ eventUUID: props.event?.uuid }),
() => ({ enabled: props.event?.uuid !== undefined })
);
const comments = computed(() => commentsResult.value?.event.comments ?? []);
const props = defineProps<{
event: IEvent;
newComment?: IComment;
}>();
const Editor = defineAsyncComponent(
() => import("@/components/TextEditor.vue")
);
const newCommentProps = computed(() => props.newComment);
const newCommentValue = ref<IComment>(new CommentModel(newCommentProps.value));
const emptyCommentError = ref(false);
const { t } = useI18n({ useScope: "global" });
watch(currentActor, () => {
newCommentValue.value.actor = currentActor.value as IPerson;
});
watch(newCommentValue, (newCommentUpdated: IComment) => {
if (emptyCommentError.value) {
emptyCommentError.value = ["", "<p></p>"].includes(newCommentUpdated.text);
}
});
const {
mutate: createCommentForEventMutation,
onDone: createCommentForEventMutationDone,
onError: createCommentForEventMutationError,
} = useMutation<
{ createComment: IComment },
{
eventId: string;
text: string;
inReplyToCommentId?: string;
isAnnouncement?: boolean;
originCommentId?: string | undefined;
}
>(CREATE_COMMENT_FROM_EVENT, () => ({
update: (
store: ApolloCache<InMemoryCache>,
{ data }: FetchResult,
{ variables }
) => {
if (data == null) return;
// comments are attached to the event, so we can pass it to replies later
const newCommentLocal = { ...data.createComment, event: props.event };
// we load all existing threads
const commentThreadsData = store.readQuery<{ event: IEvent }>({
query: COMMENTS_THREADS_WITH_REPLIES,
variables: {
eventUUID: props.event?.uuid,
},
});
if (!commentThreadsData) return;
const { event } = commentThreadsData;
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
if (variables?.originCommentId !== undefined) {
const parentCommentIndex = oldComments.findIndex(
(oldComment) => oldComment.id === variables.originCommentId
);
const parentComment = oldComments[parentCommentIndex];
// replace the root comment with has the updated list of replies in the thread list
oldComments.splice(parentCommentIndex, 1, {
...parentComment,
replies: [...parentComment.replies, newCommentLocal],
});
} else {
// otherwise it's simply a new thread and we add it to the list
oldComments.push(newCommentLocal);
}
// finally we save the thread list
store.writeQuery({
query: COMMENTS_THREADS_WITH_REPLIES,
data: {
event: {
...event,
comments: oldComments,
},
},
variables: {
eventUUID: props.event?.uuid,
},
});
},
}));
createCommentForEventMutationDone(() => {
// and reset the new comment field
newCommentValue.value = new CommentModel();
});
const notifier = inject<Notifier>("notifier");
createCommentForEventMutationError((errors) => {
console.error(errors);
if (errors.graphQLErrors && errors.graphQLErrors.length > 0) {
const error = errors.graphQLErrors[0] as AbsintheGraphQLError;
if (error.field !== "text" && error.message[0] !== "can't be blank") {
notifier?.error(error.message);
}
}
});
const createCommentForEvent = (comment: IComment) => {
emptyCommentError.value = ["", "<p></p>"].includes(comment.text);
if (emptyCommentError.value) return;
if (!comment.actor) return;
if (!props.event?.id) return;
createCommentForEventMutation({
eventId: props.event?.id,
text: comment.text,
inReplyToCommentId: comment.inReplyToComment?.id,
isAnnouncement: comment.isAnnouncement,
originCommentId: comment.originComment?.id,
});
};
const { mutate: deleteComment, onError: deleteCommentMutationError } =
useMutation<
{ deleteComment: { id: string } },
{ commentId: string; originCommentId?: string }
>(DELETE_COMMENT, () => ({
update: (
store: ApolloCache<InMemoryCache>,
{ data }: FetchResult,
{ variables }
) => {
if (data == null) return;
const deletedCommentId = data.deleteComment.id;
const commentsData = store.readQuery<{ event: IEvent }>({
query: COMMENTS_THREADS_WITH_REPLIES,
variables: {
eventUUID: props.event?.uuid,
},
});
if (!commentsData) return;
const { event } = commentsData;
let updatedComments: IComment[] = [...event.comments];
if (variables?.originCommentId) {
// we have deleted a reply to a thread
const parentCommentIndex = updatedComments.findIndex(
(oldComment) => oldComment.id === variables.originCommentId
);
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.debug("updatedComments", updatedComments);
} else {
// we have deleted a thread itself
updatedComments = updatedComments.map((reply) => {
if (reply.id === deletedCommentId) {
return {
...reply,
deletedAt: new Date().toString(),
};
}
return reply;
});
}
store.writeQuery({
query: COMMENTS_THREADS_WITH_REPLIES,
variables: {
eventUUID: props.event?.uuid,
},
data: {
event: {
...event,
comments: updatedComments,
},
},
});
},
}));
deleteCommentMutationError((error) => {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
notifier?.error(error.graphQLErrors[0].message);
}
});
const orderedComments = computed((): IComment[] => {
return comments.value
.filter((comment: IComment) => comment.inReplyToComment == null)
.sort((a: IComment, b: IComment) => {
if (a.isAnnouncement !== b.isAnnouncement) {
return (
(b.isAnnouncement === true ? 1 : 0) -
(a.isAnnouncement === true ? 1 : 0)
);
}
if (a.publishedAt && b.publishedAt) {
return (
new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
);
} else if (a.updatedAt && b.updatedAt) {
return (
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
}
return 0;
});
});
const filteredOrderedComments = computed((): IComment[] => {
return orderedComments.value.filter(
(comment) => !comment.deletedAt || comment.totalReplies > 0
);
});
const isEventOrganiser = computed((): boolean => {
const organizerId =
props.event?.organizerActor?.id || props.event?.attributedTo?.id;
return organizerId !== undefined && currentActor.value?.id === organizerId;
});
const areCommentsClosed = computed((): boolean => {
return (
currentActor.value?.id !== undefined &&
props.event?.options.commentModeration !== CommentModeration.CLOSED
);
});
const isAbleToComment = computed((): boolean => {
if (isConnected.value) {
return areCommentsClosed.value || isEventOrganiser.value;
}
return false;
});
const isConnected = computed((): boolean => {
return currentActor.value?.id != undefined;
});
</script>
<style lang="scss" scoped>
// @use "@/styles/_mixins" as *;
// form.new-comment {
// padding-bottom: 1rem;
// .media {
// flex-wrap: wrap;
// justify-content: center;
// // .media-left {
// // @include >mobile {
// // @include margin-right(0.5rem);
// // @include margin-left(0.5rem);
// // }
// // }
// .media-content {
// display: flex;
// align-items: center;
// align-content: center;
// width: min-content;
// .field {
// flex: 1;
// // @include padding-right(10px);
// margin-bottom: 0;
// &.notify-participants {
// margin-top: 0.5rem;
// }
// }
// }
// }
// }
// .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>

View File

@@ -0,0 +1,177 @@
<template>
<Story :setup-app="setupApp">
<Variant title="Basic">
<Comment
:comment="comment"
:event="event"
:currentActor="baseActor"
@create-comment="hstEvent('Create comment', $event)"
@delete-comment="hstEvent('Delete comment', $event)"
@report-comment="hstEvent('Report comment', $event)"
/>
</Variant>
<Variant title="Announcement">
<Comment
:comment="{ ...comment, isAnnouncement: true }"
:event="event"
:currentActor="baseActor"
@create-comment="hstEvent('Create comment', $event)"
@delete-comment="hstEvent('Delete comment', $event)"
@report-comment="hstEvent('Report comment', $event)"
/>
</Variant>
</Story>
</template>
<script lang="ts" setup>
import { IPerson } from "@/types/actor";
import { IComment } from "@/types/comment.model";
import {
ActorType,
CommentModeration,
EventJoinOptions,
EventStatus,
EventVisibility,
} from "@/types/enums";
import { IEvent } from "@/types/event.model";
import { reactive } from "vue";
import Comment from "./EventComment.vue";
import FloatingVue from "floating-vue";
import "floating-vue/dist/style.css";
import { hstEvent } from "histoire/client";
function setupApp({ app }) {
app.use(FloatingVue);
}
const baseActorAvatar = {
id: "",
name: "",
alt: "",
metadata: {},
url: "https://social.tcit.fr/system/accounts/avatars/000/000/001/original/a28c50ce5f2b13fd.jpg",
};
const baseActor: IPerson = {
name: "Thomas Citharel",
preferredUsername: "tcit",
avatar: baseActorAvatar,
domain: null,
url: "",
summary: "",
suspended: false,
type: ActorType.PERSON,
id: "598",
};
const baseEvent: IEvent = {
uuid: "",
title: "A very interesting event",
description: "Things happen",
beginsOn: new Date().toISOString(),
endsOn: new Date().toISOString(),
physicalAddress: {
description: "Somewhere",
street: "",
locality: "",
region: "",
country: "",
type: "",
postalCode: "",
},
picture: {
id: "",
name: "",
alt: "",
metadata: {},
url: "https://mobilizon.fr/media/81d9c76aaf740f84eefb28cf2b9988bdd2495ab1f3246159fd688e242155cb23.png?name=Screenshot_20220315_171848.png",
},
url: "",
local: true,
slug: "",
publishAt: new Date().toISOString(),
status: EventStatus.CONFIRMED,
visibility: EventVisibility.PUBLIC,
joinOptions: EventJoinOptions.FREE,
draft: false,
participantStats: {
notApproved: 0,
notConfirmed: 0,
rejected: 0,
participant: 0,
creator: 0,
moderator: 0,
administrator: 0,
going: 0,
},
participants: { total: 0, elements: [] },
relatedEvents: [],
tags: [{ slug: "something", title: "Something" }],
attributedTo: undefined,
organizerActor: {
...baseActor,
name: "Hello",
avatar: {
...baseActorAvatar,
url: "https://mobilizon.fr/media/653c2dcbb830636e0db975798163b85e038dfb7713e866e96d36bd411e105e3c.png?name=festivalsanantes%27s%20avatar.png",
},
},
comments: [],
options: {
maximumAttendeeCapacity: 0,
remainingAttendeeCapacity: 0,
showRemainingAttendeeCapacity: false,
anonymousParticipation: false,
hideOrganizerWhenGroupEvent: false,
offers: [],
participationConditions: [],
attendees: [],
program: "",
commentModeration: CommentModeration.ALLOW_ALL,
showParticipationPrice: false,
showStartTime: false,
showEndTime: false,
timezone: null,
isOnline: false,
},
metadata: [],
contacts: [],
language: "en",
category: "hello",
};
const event = reactive<IEvent>(baseEvent);
const comment = reactive<IComment>({
text: "hello",
local: true,
actor: baseActor,
totalReplies: 5,
replies: [
{
text: "a reply!",
id: "90",
actor: baseActor,
updatedAt: new Date().toISOString(),
url: "http://somewhere.tld",
replies: [],
totalReplies: 0,
isAnnouncement: false,
local: false,
},
{
text: "a reply to another reply!",
id: "92",
actor: baseActor,
updatedAt: new Date().toISOString(),
url: "http://somewhere.tld",
replies: [],
totalReplies: 0,
isAnnouncement: false,
local: false,
},
],
isAnnouncement: false,
updatedAt: new Date().toISOString(),
url: "http://somewhere.tld",
});
</script>

View File

@@ -0,0 +1,407 @@
<template>
<li
class="bg-white dark:bg-zinc-800 rounded p-2"
:class="{
reply: comment.inReplyToComment,
'bg-mbz-purple-50 dark:bg-mbz-purple-500': comment.isAnnouncement,
'!bg-mbz-bluegreen-50 dark:!bg-mbz-bluegreen-600': commentSelected,
'shadow-none': !rootComment,
}"
>
<article :id="commentId" dir="auto" class="mbz-comment">
<div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-1" v-if="actorComment">
<popover-actor-card
:actor="actorComment"
:inline="true"
v-if="!comment.deletedAt && actorComment.avatar"
>
<figure>
<img
class="rounded-xl"
:src="actorComment.avatar.url"
alt=""
width="24"
height="24"
/>
</figure>
</popover-actor-card>
<AccountCircle v-else />
<strong
v-if="!comment.deletedAt"
dir="auto"
:class="{ organizer: commentFromOrganizer }"
>{{ actorComment?.name }}</strong
>
</div>
<p v-else :href="commentURL">
<span>{{ t("[deleted]") }}</span>
</p>
<a :href="commentURL">
<small v-if="comment.updatedAt">{{
formatDistanceToNow(new Date(comment.updatedAt), {
locale: dateFnsLocale,
addSuffix: true,
})
}}</small>
</a>
</div>
<div
v-if="!comment.deletedAt"
v-html="comment.text"
dir="auto"
:lang="comment.language"
class="prose dark:prose-invert xl:prose-lg !max-w-full"
:class="{ 'text-black dark:text-white': comment.isAnnouncement }"
/>
<div v-else>{{ t("[This comment has been deleted]") }}</div>
<nav class="flex gap-1 mt-1" v-if="!comment.deletedAt">
<button
class="cursor-pointer flex hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
v-if="
currentActor?.id &&
!readOnly &&
event.options.commentModeration !== CommentModeration.CLOSED &&
!comment.deletedAt
"
@click="createReplyToComment()"
>
<Reply />
<span>{{ t("Reply") }}</span>
</button>
<o-dropdown aria-role="list" v-show="!readOnly">
<template #trigger>
<button
class="cursor-pointer flex hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
>
<DotsHorizontal />
<span class="sr-only">{{ t("More options") }}</span>
</button>
</template>
<o-dropdown-item
aria-role="listitem"
v-if="actorComment?.id === currentActor?.id"
>
<button class="flex items-center gap-1" @click="deleteComment">
<Delete :size="16" />
<span>{{ t("Delete") }}</span>
</button>
</o-dropdown-item>
<o-dropdown-item aria-role="listitem">
<button
@click="isReportModalActive = true"
class="flex items-center gap-1"
>
<Alert :size="16" />
<span>{{ t("Report") }}</span>
</button>
</o-dropdown-item>
</o-dropdown>
</nav>
<div class="" v-if="comment.totalReplies">
<button
v-if="!showReplies"
@click="showReplies = true"
class="flex cursor-pointer hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
>
<ChevronDown />
<span>{{
t(
"View a reply",
{
totalReplies: comment.totalReplies,
},
comment.totalReplies
)
}}</span>
</button>
<button
v-else-if="comment.totalReplies && showReplies"
@click="showReplies = false"
class="flex cursor-pointer hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
>
<ChevronUp />
<span>{{ t("Hide replies") }}</span>
</button>
</div>
</div>
</article>
<form
@submit.prevent="replyToComment"
v-if="currentActor?.id"
v-show="replyTo"
>
<article class="flex gap-2">
<figure v-if="currentActor?.avatar" class="mt-4">
<img
:src="currentActor?.avatar.url"
alt=""
width="48"
height="48"
class="rounded-md"
/>
</figure>
<AccountCircle v-else :size="48" />
<div class="flex-1">
<div class="flex gap-1 items-center">
<strong>{{ currentActor?.name }}</strong>
<small dir="ltr">@{{ currentActor?.preferredUsername }}</small>
</div>
<div class="flex flex-col gap-2">
<editor
ref="commentEditor"
v-model="newComment.text"
mode="comment"
:current-actor="currentActor"
:aria-label="t('Comment body')"
class="flex-1"
@submit="replyToComment"
:placeholder="t('Write a new reply')"
/>
<o-button
:disabled="newComment.text.trim().length === 0"
native-type="submit"
variant="primary"
class="self-end"
>{{ t("Post a reply") }}</o-button
>
</div>
</div>
</article>
</form>
<transition-group
name="comment-replies"
v-if="showReplies"
tag="ul"
class="flex flex-col gap-2"
>
<EventComment
v-for="reply in comment.replies"
:key="reply.id"
:comment="reply"
:event="event"
:currentActor="currentActor"
:rootComment="false"
@create-comment="emit('create-comment', $event)"
@delete-comment="emit('delete-comment', $event)"
@report-comment="emit('report-comment', $event)"
class="ml-2"
/>
</transition-group>
<o-modal
v-model:active="isReportModalActive"
has-modal-card
ref="reportModal"
:close-button-aria-label="t('Close')"
:autoFocus="false"
:trapFocus="false"
>
<ReportModal
:on-confirm="reportComment"
:title="t('Report this comment')"
:outside-domain="comment.actor?.domain"
/>
</o-modal>
</li>
</template>
<script lang="ts" setup>
import type EditorComponent from "@/components/TextEditor.vue";
import { formatDistanceToNow } from "date-fns";
import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model";
import { IPerson } from "../../types/actor";
import { IEvent } from "../../types/event.model";
import PopoverActorCard from "../Account/PopoverActorCard.vue";
import {
computed,
defineAsyncComponent,
inject,
onMounted,
ref,
nextTick,
} from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import Delete from "vue-material-design-icons/Delete.vue";
import Alert from "vue-material-design-icons/Alert.vue";
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
import DotsHorizontal from "vue-material-design-icons/DotsHorizontal.vue";
import Reply from "vue-material-design-icons/Reply.vue";
import type { Locale } from "date-fns";
import ReportModal from "@/components/Report/ReportModal.vue";
import { useCreateReport } from "@/composition/apollo/report";
import { Snackbar } from "@/plugins/snackbar";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import RouteName from "@/router/name";
const router = useRouter();
const Editor = defineAsyncComponent(
() => import("@/components/TextEditor.vue")
);
const props = withDefaults(
defineProps<{
comment: IComment;
event: IEvent;
currentActor: IPerson;
rootComment?: boolean;
readOnly: boolean;
}>(),
{ rootComment: true, readOnly: false }
);
const event = computed(() => props.event);
const emit = defineEmits<{
(e: "create-comment", comment: IComment): void;
(e: "delete-comment", comment: IComment): void;
(e: "report-comment", comment: IComment): void;
}>();
const commentEditor = ref<typeof EditorComponent | null>(null);
const newComment = ref<IComment>(new CommentModel());
const replyTo = ref(false);
const showReplies = ref(false);
const route = useRoute();
const { t } = useI18n({ useScope: "global" });
const isReportModalActive = ref(false);
onMounted(() => {
if (route?.hash.includes(`#comment-${props.comment.uuid}`)) {
showReplies.value = true;
}
});
const createReplyToComment = async (): Promise<void> => {
if (replyTo.value) {
replyTo.value = false;
newComment.value = new CommentModel();
return;
}
replyTo.value = true;
if (props.comment.actor) {
commentEditor.value?.replyToComment(props.comment.actor);
await nextTick(); // wait for the mention to be injected
commentEditor.value?.focus();
}
};
const replyToComment = (): void => {
newComment.value.inReplyToComment = props.comment;
newComment.value.originComment = props.comment.originComment ?? props.comment;
newComment.value.actor = props.currentActor;
console.debug(newComment.value);
emit("create-comment", newComment.value);
newComment.value = new CommentModel();
replyTo.value = false;
showReplies.value = true;
};
const deleteComment = (): void => {
emit("delete-comment", props.comment);
showReplies.value = false;
};
const commentSelected = computed((): boolean => {
return `#${commentId.value}` === route?.hash;
});
const commentFromOrganizer = computed((): boolean => {
const organizerId =
props.event?.organizerActor?.id || props.event?.attributedTo?.id;
return organizerId !== undefined && props.comment?.actor?.id === organizerId;
});
const commentId = computed((): string => {
if (props.comment.originComment)
return `comment-${props.comment.originComment.uuid}-${props.comment.uuid}`;
return `comment-${props.comment.uuid}`;
});
const commentURL = computed((): string => {
if (!props.comment.local && props.comment.url) return props.comment.url;
return (
router.resolve({
name: RouteName.EVENT,
params: { uuid: event.value.uuid },
}).href + `#${commentId.value}`
);
});
const reportModal = (): void => {
if (!props.comment.actor) return;
emit("report-comment", props.comment);
// this.$buefy.modal.open({
// component: ReportModal,
// props: {
// title: t("Report this comment"),
// comment: props.comment,
// onConfirm: reportComment,
// outsideDomain: props.comment.actor?.domain,
// },
// // https://github.com/buefy/buefy/pull/3589
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// // @ts-ignore
// closeButtonAriaLabel: this.t("Close"),
// });
};
const {
mutate: createReportMutation,
onError: onCreateReportError,
onDone: oneCreateReportDone,
} = useCreateReport();
const reportComment = async (
content: string,
forward: boolean
): Promise<void> => {
if (!props.comment.actor) return;
createReportMutation({
reportedId: props.comment.actor?.id ?? "",
commentsIds: [props.comment.id ?? ""],
content,
forward,
});
};
const snackbar = inject<Snackbar>("snackbar");
const { oruga } = useProgrammatic();
onCreateReportError((e) => {
isReportModalActive.value = false;
if (e.message) {
snackbar?.open({
message: e.message,
variant: "danger",
position: "bottom",
});
}
});
oneCreateReportDone(() => {
isReportModalActive.value = false;
oruga.notification.open({
message: t("Comment from {'@'}{username} reported", {
username: props.comment.actor?.preferredUsername,
}),
variant: "success",
position: "bottom-right",
duration: 5000,
});
});
const actorComment = computed(() => props.comment.actor);
const dateFnsLocale = inject<Locale>("dateFnsLocale");
</script>
<style>
article.mbz-comment .mention.h-card {
@apply inline-block border border-zinc-600 dark:border-zinc-300 rounded py-0.5 px-1;
}
</style>