Migrate to Vue 3 and Vite
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -1,45 +1,50 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit.prevent="publish(false)" v-if="isCurrentActorAGroupModerator">
|
||||
<div class="container section">
|
||||
<div class="container mx-auto">
|
||||
<breadcrumbs-nav v-if="actualGroup" :links="breadcrumbLinks" />
|
||||
<h1 class="title" v-if="isUpdate === true">
|
||||
<h1 v-if="isUpdate === true">
|
||||
{{ $t("Edit post") }}
|
||||
</h1>
|
||||
<h1 class="title" v-else>
|
||||
<h1 v-else>
|
||||
{{ $t("Add a new post") }}
|
||||
</h1>
|
||||
<subtitle>{{ $t("General information") }}</subtitle>
|
||||
<h2>{{ $t("General information") }}</h2>
|
||||
<picture-upload
|
||||
v-model="pictureFile"
|
||||
:textFallback="$t('Headline picture')"
|
||||
:defaultImage="editablePost.picture"
|
||||
/>
|
||||
|
||||
<b-field
|
||||
<o-field
|
||||
:label="$t('Title')"
|
||||
label-for="post-title"
|
||||
:type="errors.title ? 'is-danger' : null"
|
||||
:message="errors.title"
|
||||
>
|
||||
<b-input
|
||||
size="is-large"
|
||||
<o-input
|
||||
size="large"
|
||||
aria-required="true"
|
||||
required
|
||||
v-model="editablePost.title"
|
||||
id="post-title"
|
||||
dir="auto"
|
||||
/>
|
||||
</b-field>
|
||||
</o-field>
|
||||
|
||||
<tag-input v-model="editablePost.tags" />
|
||||
<tag-input v-model="editablePost.tags" :fetch-tags="fetchTags" />
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t("Post") }}</label>
|
||||
<o-field :label="t('Post')">
|
||||
<p v-if="errors.body" class="help is-danger">{{ errors.body }}</p>
|
||||
<editor v-model="editablePost.body" :aria-label="$t('Post body')" />
|
||||
</div>
|
||||
<subtitle>{{ $t("Who can view this post") }}</subtitle>
|
||||
<editor
|
||||
class="w-full"
|
||||
v-if="currentActor"
|
||||
v-model="editablePost.body"
|
||||
:aria-label="$t('Post body')"
|
||||
:current-actor="currentActor"
|
||||
/>
|
||||
</o-field>
|
||||
<h2 class="mt-2">{{ $t("Who can view this post") }}</h2>
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{
|
||||
@@ -49,358 +54,380 @@
|
||||
}}
|
||||
</legend>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
<o-radio
|
||||
v-model="editablePost.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.PUBLIC"
|
||||
>{{ $t("Visible everywhere on the web") }}</b-radio
|
||||
>{{ $t("Visible everywhere on the web") }}</o-radio
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
<o-radio
|
||||
v-model="editablePost.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.UNLISTED"
|
||||
>{{ $t("Only accessible through link") }}</b-radio
|
||||
>{{ $t("Only accessible through link") }}</o-radio
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
<o-radio
|
||||
v-model="editablePost.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.PRIVATE"
|
||||
>{{ $t("Only accessible to members of the group") }}</b-radio
|
||||
>{{ $t("Only accessible to members of the group") }}</o-radio
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-end">
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-text" @click="$router.go(-1)">{{
|
||||
$t("Cancel")
|
||||
}}</b-button>
|
||||
</span>
|
||||
<span class="navbar-item" v-if="this.isUpdate">
|
||||
<b-button
|
||||
type="is-danger is-outlined"
|
||||
@click="openDeletePostModal"
|
||||
>{{ $t("Delete post") }}</b-button
|
||||
>
|
||||
</span>
|
||||
<div class="container mx-auto">
|
||||
<div class="navbar-menu flex flex-wrap py-2">
|
||||
<div class="flex flex-wrap justify-end ml-auto gap-1">
|
||||
<o-button type="is-text" @click="$router.go(-1)">{{
|
||||
$t("Cancel")
|
||||
}}</o-button>
|
||||
<o-button
|
||||
v-if="isUpdate"
|
||||
type="is-danger is-outlined"
|
||||
@click="openDeletePostModal"
|
||||
>{{ $t("Delete post") }}</o-button
|
||||
>
|
||||
<!-- If an post has been published we can't make it draft anymore -->
|
||||
<span class="navbar-item" v-if="post.draft === true">
|
||||
<b-button type="is-primary" outlined @click="publish(true)">{{
|
||||
$t("Save draft")
|
||||
}}</b-button>
|
||||
</span>
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-primary" native-type="submit">
|
||||
<span v-if="isUpdate === false || post.draft === true">{{
|
||||
$t("Publish")
|
||||
}}</span>
|
||||
<o-button
|
||||
variant="primary"
|
||||
v-if="post?.draft === true"
|
||||
outlined
|
||||
@click="publish(true)"
|
||||
>{{ $t("Save draft") }}</o-button
|
||||
>
|
||||
<o-button variant="primary" native-type="submit">
|
||||
<span v-if="isUpdate === false || post?.draft === true">{{
|
||||
$t("Publish")
|
||||
}}</span>
|
||||
|
||||
<span v-else>{{ $t("Update post") }}</span>
|
||||
</b-button>
|
||||
</span>
|
||||
<span v-else>{{ $t("Update post") }}</span>
|
||||
</o-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</form>
|
||||
<b-loading
|
||||
v-else-if="$apollo.loading"
|
||||
<o-loading
|
||||
v-else-if="postLoading"
|
||||
:is-full-page="false"
|
||||
:active.sync="$apollo.loading"
|
||||
v-model:active="postLoading"
|
||||
:can-cancel="false"
|
||||
></b-loading>
|
||||
<div class="container section" v-else>
|
||||
<b-message type="is-danger">
|
||||
></o-loading>
|
||||
<div class="container mx-auto" v-else>
|
||||
<o-notification variant="danger">
|
||||
{{ $t("Only group moderators can create, edit and delete posts.") }}
|
||||
</b-message>
|
||||
</o-notification>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||
import { mixins } from "vue-class-component";
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
buildFileFromIMedia,
|
||||
buildFileVariable,
|
||||
readFileAsync,
|
||||
} from "@/utils/image";
|
||||
import GroupMixin from "@/mixins/group";
|
||||
import { PostVisibility } from "@/types/enums";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { CREATE_POST, UPDATE_POST } from "../../graphql/post";
|
||||
import { MemberRole, PostVisibility } from "@/types/enums";
|
||||
import {
|
||||
CREATE_POST,
|
||||
DELETE_POST,
|
||||
FETCH_POST,
|
||||
UPDATE_POST,
|
||||
} from "../../graphql/post";
|
||||
|
||||
import { IPost } from "../../types/post.model";
|
||||
import Editor from "../../components/Editor.vue";
|
||||
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
||||
import TagInput from "../../components/Event/TagInput.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||
import PictureUpload from "../../components/PictureUpload.vue";
|
||||
import { PERSON_STATUS_GROUP } from "@/graphql/actor";
|
||||
import { FETCH_GROUP } from "@/graphql/group";
|
||||
import PostMixin from "../../mixins/post";
|
||||
import { useGroup } from "@/composition/apollo/group";
|
||||
import {
|
||||
useCurrentActorClient,
|
||||
usePersonStatusGroup,
|
||||
} from "@/composition/apollo/actor";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, inject, onMounted, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { fetchTags } from "@/composition/apollo/tags";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
config: CONFIG,
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.preferredUsername,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.preferredUsername;
|
||||
},
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
slug?: string;
|
||||
preferredUsername?: string;
|
||||
isUpdate?: boolean;
|
||||
}>(),
|
||||
{ isUpdate: false }
|
||||
);
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
const { group } = useGroup(props.preferredUsername);
|
||||
|
||||
const { result: postResult, loading: postLoading } = useQuery<{
|
||||
post: IPost;
|
||||
}>(FETCH_POST, () => ({ slug: props.slug }));
|
||||
|
||||
const post = computed(() => postResult.value?.post);
|
||||
|
||||
const pictureFile = ref<File | null>(null);
|
||||
const errors = ref<Record<string, unknown>>({});
|
||||
const editablePost = ref<IPost>({
|
||||
title: "",
|
||||
body: "",
|
||||
local: true,
|
||||
draft: true,
|
||||
visibility: PostVisibility.PUBLIC,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
pictureFile.value = await buildFileFromIMedia(post.value?.picture);
|
||||
});
|
||||
|
||||
watch(post, async (newPost: IPost, oldPost: IPost) => {
|
||||
if (oldPost?.picture !== newPost.picture) {
|
||||
pictureFile.value = await buildFileFromIMedia(post.value?.picture);
|
||||
}
|
||||
editablePost.value = { ...post.value };
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { mutate: updatePost, onDone: onUpdateDone } = useMutation<{
|
||||
updatePost: IPost;
|
||||
}>(UPDATE_POST);
|
||||
const {
|
||||
mutate: createPost,
|
||||
onDone: onCreateDone,
|
||||
onError: onCreateError,
|
||||
} = useMutation<{
|
||||
createPost: IPost;
|
||||
}>(CREATE_POST);
|
||||
|
||||
onUpdateDone(({ data }) => {
|
||||
if (data && data.updatePost) {
|
||||
router.push({
|
||||
name: RouteName.POST,
|
||||
params: { slug: data.updatePost.slug },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onCreateDone(({ data }) => {
|
||||
if (data && data.createPost) {
|
||||
router.push({
|
||||
name: RouteName.POST,
|
||||
params: { slug: data.createPost.slug },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onCreateError((error) => {
|
||||
console.error(error);
|
||||
errors.value = error.graphQLErrors.reduce(
|
||||
(acc: { [key: string]: any }, localError: any) => {
|
||||
acc[localError.field] = transformMessage(localError.message);
|
||||
return acc;
|
||||
},
|
||||
person: {
|
||||
query: PERSON_STATUS_GROUP,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
id: this.currentActor.id,
|
||||
group: usernameWithDomain(this.actualGroup),
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor?.id || !this.actualGroup?.preferredUsername;
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
const publish = async (draft: boolean): Promise<void> => {
|
||||
errors.value = {};
|
||||
|
||||
if (props.isUpdate) {
|
||||
updatePost({
|
||||
id: editablePost.value?.id,
|
||||
title: editablePost.value?.title,
|
||||
body: editablePost.value?.body,
|
||||
tags: (editablePost.value?.tags || []).map(({ title }) => title),
|
||||
visibility: editablePost.value?.visibility,
|
||||
draft,
|
||||
...(await buildPicture()),
|
||||
});
|
||||
} else {
|
||||
createPost({
|
||||
...editablePost.value,
|
||||
...(await buildPicture()),
|
||||
tags: (editablePost.value?.tags ?? []).map(({ title }) => title),
|
||||
attributedToId: actualGroup.value.id,
|
||||
draft,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const transformMessage = (message: string[] | string): string | undefined => {
|
||||
if (Array.isArray(message) && message.length > 0) {
|
||||
return message[0];
|
||||
}
|
||||
if (typeof message === "string") {
|
||||
return message;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buildPicture = async (): Promise<Record<string, unknown>> => {
|
||||
let obj: { picture?: any } = {};
|
||||
if (pictureFile.value) {
|
||||
const pictureObj = buildFileVariable(pictureFile.value, "picture");
|
||||
obj = { ...obj, ...pictureObj };
|
||||
}
|
||||
try {
|
||||
if (editablePost.value?.picture && pictureFile.value) {
|
||||
const oldPictureFile = (await buildFileFromIMedia(
|
||||
editablePost.value.picture
|
||||
)) as File;
|
||||
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
||||
const newPictureFileContent = await readFileAsync(
|
||||
pictureFile.value as File
|
||||
);
|
||||
if (oldPictureFileContent === newPictureFileContent) {
|
||||
obj.picture = { mediaId: editablePost.value.picture.id };
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
const actualGroup = computed((): IActor => {
|
||||
if (!group.value?.id) {
|
||||
return post.value?.attributedTo as IActor;
|
||||
}
|
||||
return group.value;
|
||||
});
|
||||
|
||||
const actualPreferredUsername = computed(() =>
|
||||
usernameWithDomain(actualGroup.value)
|
||||
);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const breadcrumbLinks = computed(() => {
|
||||
const links = [
|
||||
{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(actualGroup.value),
|
||||
},
|
||||
text: displayName(actualGroup.value),
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Editor,
|
||||
TagInput,
|
||||
Subtitle,
|
||||
PictureUpload,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
title: this.isUpdate
|
||||
? (this.$t("Edit post") as string)
|
||||
: (this.$t("Add a new post") as string),
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class EditPost extends mixins(GroupMixin, PostMixin) {
|
||||
@Prop({ required: false, type: String }) slug: undefined | string;
|
||||
|
||||
@Prop({ required: false, type: String }) preferredUsername!: string;
|
||||
|
||||
@Prop({ type: Boolean, default: false }) isUpdate!: boolean;
|
||||
|
||||
post: IPost = {
|
||||
title: "",
|
||||
body: "",
|
||||
local: true,
|
||||
draft: true,
|
||||
visibility: PostVisibility.PUBLIC,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
PostVisibility = PostVisibility;
|
||||
|
||||
pictureFile: File | null = null;
|
||||
|
||||
errors: Record<string, unknown> = {};
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
editablePost: IPost = this.post;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
async mounted(): Promise<void> {
|
||||
this.pictureFile = await buildFileFromIMedia(this.post.picture);
|
||||
}
|
||||
|
||||
@Watch("post")
|
||||
async updatePostPicture(oldPost: IPost, newPost: IPost): Promise<void> {
|
||||
if (oldPost.picture !== newPost.picture) {
|
||||
this.pictureFile = await buildFileFromIMedia(this.post.picture);
|
||||
}
|
||||
this.editablePost = { ...this.post };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
async publish(draft: boolean): Promise<void> {
|
||||
this.errors = {};
|
||||
|
||||
if (this.isUpdate) {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: UPDATE_POST,
|
||||
variables: {
|
||||
id: this.editablePost.id,
|
||||
title: this.editablePost.title,
|
||||
body: this.editablePost.body,
|
||||
tags: (this.editablePost.tags || []).map(({ title }) => title),
|
||||
visibility: this.editablePost.visibility,
|
||||
draft,
|
||||
...(await this.buildPicture()),
|
||||
},
|
||||
});
|
||||
if (data && data.updatePost) {
|
||||
this.$router.push({
|
||||
name: RouteName.POST,
|
||||
params: { slug: data.updatePost.slug },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: CREATE_POST,
|
||||
variables: {
|
||||
...this.editablePost,
|
||||
...(await this.buildPicture()),
|
||||
tags: (this.editablePost.tags || []).map(({ title }) => title),
|
||||
attributedToId: this.actualGroup.id,
|
||||
draft,
|
||||
},
|
||||
});
|
||||
if (data && data.createPost) {
|
||||
this.$router.push({
|
||||
name: RouteName.POST,
|
||||
params: { slug: data.createPost.slug },
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
this.errors = error.graphQLErrors.reduce(
|
||||
(acc: { [key: string]: any }, localError: any) => {
|
||||
acc[localError.field] = EditPost.transformMessage(
|
||||
localError.message
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static transformMessage(message: string[] | string): string | undefined {
|
||||
if (Array.isArray(message) && message.length > 0) {
|
||||
return message[0];
|
||||
}
|
||||
if (typeof message === "string") {
|
||||
return message;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async buildPicture(): Promise<Record<string, unknown>> {
|
||||
let obj: { picture?: any } = {};
|
||||
if (this.pictureFile) {
|
||||
const pictureObj = buildFileVariable(this.pictureFile, "picture");
|
||||
obj = { ...obj, ...pictureObj };
|
||||
}
|
||||
try {
|
||||
if (this.editablePost.picture && this.pictureFile) {
|
||||
const oldPictureFile = (await buildFileFromIMedia(
|
||||
this.editablePost.picture
|
||||
)) as File;
|
||||
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
||||
const newPictureFileContent = await readFileAsync(
|
||||
this.pictureFile as File
|
||||
);
|
||||
if (oldPictureFileContent === newPictureFileContent) {
|
||||
obj.picture = { mediaId: this.editablePost.picture.id };
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
get actualGroup(): IActor {
|
||||
if (!this.group?.id) {
|
||||
return this.post.attributedTo as IActor;
|
||||
}
|
||||
return this.group;
|
||||
}
|
||||
|
||||
get breadcrumbLinks() {
|
||||
const links = [
|
||||
{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(this.actualGroup),
|
||||
},
|
||||
text: displayName(this.actualGroup),
|
||||
{
|
||||
name: RouteName.POSTS,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(actualGroup.value),
|
||||
},
|
||||
{
|
||||
name: RouteName.POSTS,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(this.actualGroup),
|
||||
},
|
||||
text: this.$t("Posts"),
|
||||
},
|
||||
];
|
||||
if (this.preferredUsername) {
|
||||
links.push({
|
||||
text: this.$t("New post") as string,
|
||||
name: RouteName.POST_EDIT,
|
||||
params: { preferredUsername: usernameWithDomain(this.actualGroup) },
|
||||
});
|
||||
} else {
|
||||
links.push({
|
||||
text: this.$t("Edit post") as string,
|
||||
name: RouteName.POST_EDIT,
|
||||
params: { preferredUsername: usernameWithDomain(this.actualGroup) },
|
||||
});
|
||||
}
|
||||
return links;
|
||||
text: t("Posts"),
|
||||
},
|
||||
];
|
||||
if (props.preferredUsername) {
|
||||
links.push({
|
||||
text: t("New post") as string,
|
||||
name: RouteName.POST_EDIT,
|
||||
params: { preferredUsername: usernameWithDomain(actualGroup.value) },
|
||||
});
|
||||
} else {
|
||||
links.push({
|
||||
text: t("Edit post") as string,
|
||||
name: RouteName.POST_EDIT,
|
||||
params: { preferredUsername: usernameWithDomain(actualGroup.value) },
|
||||
});
|
||||
}
|
||||
}
|
||||
return links;
|
||||
});
|
||||
|
||||
const isCurrentActorAGroupModerator = computed((): boolean => {
|
||||
return hasCurrentActorThisRole([
|
||||
MemberRole.MODERATOR,
|
||||
MemberRole.ADMINISTRATOR,
|
||||
]);
|
||||
});
|
||||
|
||||
const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
|
||||
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
|
||||
return (
|
||||
personMemberships.value?.total > 0 &&
|
||||
roles.includes(personMemberships.value?.elements[0].role)
|
||||
);
|
||||
};
|
||||
|
||||
const { person } = usePersonStatusGroup(actualPreferredUsername);
|
||||
|
||||
const personMemberships = computed(
|
||||
() => person.value?.memberships ?? { total: 0, elements: [] }
|
||||
);
|
||||
|
||||
const dialog = inject<Dialog>("dialog");
|
||||
|
||||
const openDeletePostModal = async (): Promise<void> => {
|
||||
dialog?.confirm({
|
||||
type: "danger",
|
||||
title: t("Delete post"),
|
||||
message: t(
|
||||
"Are you sure you want to delete this post? This action cannot be reverted."
|
||||
),
|
||||
onConfirm: () =>
|
||||
deletePost({
|
||||
id: post.value?.id,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: deletePost,
|
||||
onDone: onDeletePostDone,
|
||||
onError: onDeletePostError,
|
||||
} = useMutation(DELETE_POST);
|
||||
|
||||
onDeletePostDone(({ data }) => {
|
||||
if (data && post.value?.attributedTo) {
|
||||
router.push({
|
||||
name: RouteName.POSTS,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(post.value?.attributedTo),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: computed(() =>
|
||||
props.isUpdate ? t("Edit post") : t("Add a new post")
|
||||
),
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/_mixins" as *;
|
||||
.container.section {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
form {
|
||||
nav.navbar {
|
||||
min-height: 2rem !important;
|
||||
// min-height: 2rem !important;
|
||||
background: lighten($secondary, 10%);
|
||||
|
||||
.container {
|
||||
min-height: 2rem;
|
||||
// min-height: 2rem;
|
||||
|
||||
.navbar-menu,
|
||||
.navbar-end {
|
||||
display: flex !important;
|
||||
// display: flex !important;
|
||||
background: lighten($secondary, 10%);
|
||||
flex-wrap: wrap;
|
||||
// flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.navbar-end {
|
||||
justify-content: flex-end;
|
||||
@include margin-left(auto);
|
||||
// justify-content: flex-end;
|
||||
// @include margin-left(auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
margin: 15px 0 7.5px;
|
||||
}
|
||||
|
||||
legend {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb li.is-active > span {
|
||||
padding: 0 0.75em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container section" v-if="group">
|
||||
<div class="container mx-auto section" v-if="group">
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{
|
||||
@@ -26,14 +26,16 @@
|
||||
<p v-if="isCurrentActorMember">
|
||||
{{ $t("Only group moderators can create, edit and delete posts.") }}
|
||||
</p>
|
||||
<router-link
|
||||
<o-button
|
||||
tag="router-link"
|
||||
v-if="isCurrentActorAGroupModerator"
|
||||
:to="{
|
||||
name: RouteName.POST_CREATE,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
class="button is-primary"
|
||||
>{{ $t("+ Create a post") }}</router-link
|
||||
variant="primary"
|
||||
class="my-2"
|
||||
>{{ $t("+ Create a post") }}</o-button
|
||||
>
|
||||
</div>
|
||||
<div class="post-list">
|
||||
@@ -42,14 +44,18 @@
|
||||
:isCurrentActorMember="isCurrentActorMember"
|
||||
/>
|
||||
</div>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<b-message
|
||||
v-if="group.posts.elements.length === 0 && $apollo.loading === false"
|
||||
type="is-danger"
|
||||
<o-loading v-model:active="loading"></o-loading>
|
||||
<o-notification
|
||||
v-if="
|
||||
group.posts.elements.length === 0 &&
|
||||
membershipsLoading === false &&
|
||||
groupLoading === false
|
||||
"
|
||||
variant="danger"
|
||||
>
|
||||
{{ $t("No posts found") }}
|
||||
</b-message>
|
||||
<b-pagination
|
||||
</o-notification>
|
||||
<o-pagination
|
||||
:total="group.posts.total"
|
||||
v-model="postsPage"
|
||||
:per-page="POSTS_PAGE_LIMIT"
|
||||
@@ -58,105 +64,92 @@
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
>
|
||||
</b-pagination>
|
||||
</o-pagination>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop } from "vue-property-decorator";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { mixins } from "vue-class-component";
|
||||
import GroupMixin from "@/mixins/group";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
<script lang="ts" setup>
|
||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { FETCH_GROUP_POSTS } from "../../graphql/post";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
import { IPost } from "../../types/post.model";
|
||||
import { usernameWithDomain, displayName } from "../../types/actor";
|
||||
import {
|
||||
usernameWithDomain,
|
||||
displayName,
|
||||
IPerson,
|
||||
IGroup,
|
||||
} from "../../types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import MultiPostListItem from "../../components/Post/MultiPostListItem.vue";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
|
||||
const props = defineProps<{ preferredUsername: string }>();
|
||||
|
||||
const postsPage = useRouteQuery("page", 1, integerTransformer);
|
||||
const POSTS_PAGE_LIMIT = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
memberships: {
|
||||
query: PERSON_MEMBERSHIPS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
id: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: (data) => data.person.memberships.elements,
|
||||
skip() {
|
||||
return !this.currentActor || !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
group: {
|
||||
query: FETCH_GROUP_POSTS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
preferredUsername: this.preferredUsername,
|
||||
page: this.postsPage,
|
||||
limit: POSTS_PAGE_LIMIT,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MultiPostListItem,
|
||||
},
|
||||
metaInfo() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { group } = this;
|
||||
return {
|
||||
title: this.$t("{group} posts", {
|
||||
group: group?.name || usernameWithDomain(group),
|
||||
}) as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class PostList extends mixins(GroupMixin) {
|
||||
@Prop({ required: true, type: String }) preferredUsername!: string;
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
posts!: Paginate<IPost>;
|
||||
const { result: membershipsResult, loading: membershipsLoading } = useQuery<{
|
||||
person: Pick<IPerson, "memberships">;
|
||||
}>(
|
||||
PERSON_MEMBERSHIPS,
|
||||
() => ({ id: currentActor.value?.id }),
|
||||
() => ({ enabled: currentActor.value?.id !== undefined })
|
||||
);
|
||||
const memberships = computed(() => membershipsResult.value?.person.memberships);
|
||||
|
||||
memberships!: IMember[];
|
||||
const { result: groupPostsResult, loading: groupLoading } = useQuery<{
|
||||
group: IGroup;
|
||||
}>(
|
||||
FETCH_GROUP_POSTS,
|
||||
() => ({
|
||||
preferredUsername: props.preferredUsername,
|
||||
page: postsPage.value,
|
||||
limit: POSTS_PAGE_LIMIT,
|
||||
}),
|
||||
() => ({ enabled: props.preferredUsername !== undefined })
|
||||
);
|
||||
|
||||
postsPage = 1;
|
||||
const group = computed(() => groupPostsResult.value?.group);
|
||||
|
||||
RouteName = RouteName;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
useHead({
|
||||
title: computed(() =>
|
||||
t("{group} posts", {
|
||||
group: displayName(group.value),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
displayName = displayName;
|
||||
const loading = computed(() => membershipsLoading.value || groupLoading.value);
|
||||
|
||||
POSTS_PAGE_LIMIT = POSTS_PAGE_LIMIT;
|
||||
const isCurrentActorMember = computed((): boolean => {
|
||||
if (!group.value || !memberships.value) return false;
|
||||
return memberships.value.elements
|
||||
.map(({ parent: { id } }) => id)
|
||||
.includes(group.value.id);
|
||||
});
|
||||
|
||||
get isCurrentActorMember(): boolean {
|
||||
if (!this.group || !this.memberships) return false;
|
||||
return this.memberships
|
||||
.map(({ parent: { id } }) => id)
|
||||
.includes(this.group.id);
|
||||
}
|
||||
}
|
||||
const isCurrentActorAGroupModerator = computed((): boolean => {
|
||||
return hasCurrentActorThisRole([
|
||||
MemberRole.MODERATOR,
|
||||
MemberRole.ADMINISTRATOR,
|
||||
]);
|
||||
});
|
||||
|
||||
const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
|
||||
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
|
||||
return (
|
||||
memberships.value !== undefined &&
|
||||
memberships.value?.total > 0 &&
|
||||
roles.includes(memberships.value?.elements[0].role)
|
||||
);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.container.section {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
section {
|
||||
div.intro,
|
||||
.post-list {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<template>
|
||||
<article class="container post" v-if="post">
|
||||
<article class="container mx-auto post" v-if="post">
|
||||
<header>
|
||||
<div class="banner-container">
|
||||
<div class="flex justify-center">
|
||||
<lazy-image-wrapper :picture="post.picture" />
|
||||
</div>
|
||||
<div class="heading-section">
|
||||
<div class="heading-wrapper" dir="auto">
|
||||
<div class="title-metadata">
|
||||
<div class="title-wrapper">
|
||||
<div class="relative flex flex-col">
|
||||
<div
|
||||
class="px-2 py-3 flex flex-wrap justify-center items-center"
|
||||
dir="auto"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="inline">
|
||||
<b-tag
|
||||
class="mr-2"
|
||||
type="is-warning"
|
||||
variant="warning"
|
||||
size="is-medium"
|
||||
v-if="post.draft"
|
||||
>{{ $t("Draft") }}</b-tag
|
||||
>
|
||||
<h1 class="title text-3xl" :lang="post.language">
|
||||
<h1 class="inline" :lang="post.language">
|
||||
{{ post.title }}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="metadata">
|
||||
<p class="mt-2 flex flex-col flex-wrap justify-start">
|
||||
<router-link
|
||||
slot="author"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
@@ -29,65 +31,65 @@
|
||||
},
|
||||
}"
|
||||
>
|
||||
<actor-inline :actor="post.attributedTo" />
|
||||
<actor-inline
|
||||
v-if="post.attributedTo"
|
||||
:actor="post.attributedTo"
|
||||
/>
|
||||
</router-link>
|
||||
<span class="published has-text-grey-dark" v-if="!post.draft">
|
||||
<b-icon icon="clock" size="is-small" />
|
||||
{{ post.publishAt | formatDateTimeString }}
|
||||
<span
|
||||
class="inline-flex gap-2 items-center mt-2"
|
||||
v-if="!post.draft && post.publishAt"
|
||||
>
|
||||
<Clock :size="16" />
|
||||
{{ formatDateTimeString(post.publishAt) }}
|
||||
</span>
|
||||
<span
|
||||
class="published has-text-grey-dark"
|
||||
class="inline-flex gap-2 items-center mt-2"
|
||||
:title="
|
||||
$options.filters.formatDateTimeString(
|
||||
post.updatedAt,
|
||||
undefined,
|
||||
true,
|
||||
'short'
|
||||
)
|
||||
formatDateTimeString(post.updatedAt, undefined, true, 'short')
|
||||
"
|
||||
v-else
|
||||
v-else-if="post.updatedAt"
|
||||
>
|
||||
<b-icon icon="clock" size="is-small" />
|
||||
<Clock :size="16" />
|
||||
{{
|
||||
$t("Edited {relative_time} ago", {
|
||||
relative_time: formatDistanceToNowStrict(
|
||||
new Date(post.updatedAt),
|
||||
{
|
||||
locale: $dateFnsLocale,
|
||||
locale: dateFnsLocale,
|
||||
}
|
||||
),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
v-if="post.visibility === PostVisibility.UNLISTED"
|
||||
class="has-text-grey-dark"
|
||||
>
|
||||
<b-icon icon="link" size="is-small" />
|
||||
<span v-if="post.visibility === PostVisibility.UNLISTED" class="">
|
||||
<o-icon icon="link" size="small" />
|
||||
{{ $t("Accessible only by link") }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="post.visibility === PostVisibility.PRIVATE"
|
||||
class="has-text-grey-dark"
|
||||
class=""
|
||||
>
|
||||
<b-icon icon="lock" size="is-small" />
|
||||
<Lock :size="16" />
|
||||
{{
|
||||
$t("Accessible only to members", {
|
||||
group: post.attributedTo.name,
|
||||
group: post.attributedTo?.name,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<b-dropdown position="is-bottom-left" aria-role="list">
|
||||
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
|
||||
{{ $t("Actions") }}
|
||||
</b-button>
|
||||
<b-dropdown-item
|
||||
<o-dropdown position="bottom-left" aria-role="list">
|
||||
<template #trigger>
|
||||
<o-button role="button" icon-right="dots-horizontal">
|
||||
{{ $t("Actions") }}
|
||||
</o-button>
|
||||
</template>
|
||||
<o-dropdown-item
|
||||
aria-role="listitem"
|
||||
has-link
|
||||
v-if="
|
||||
currentActor.id === post.author.id ||
|
||||
currentActor?.id === post?.author?.id ||
|
||||
isCurrentActorAGroupModerator
|
||||
"
|
||||
>
|
||||
@@ -96,32 +98,32 @@
|
||||
name: RouteName.POST_EDIT,
|
||||
params: { slug: post.slug },
|
||||
}"
|
||||
>{{ $t("Edit") }} <b-icon icon="pencil"
|
||||
>{{ $t("Edit") }} <o-icon icon="pencil"
|
||||
/></router-link>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
</o-dropdown-item>
|
||||
<o-dropdown-item
|
||||
aria-role="listitem"
|
||||
v-if="
|
||||
currentActor.id === post.author.id ||
|
||||
currentActor?.id === post?.author?.id ||
|
||||
isCurrentActorAGroupModerator
|
||||
"
|
||||
@click="openDeletePostModal"
|
||||
@keyup.enter="openDeletePostModal"
|
||||
>
|
||||
{{ $t("Delete") }}
|
||||
<b-icon icon="delete" />
|
||||
</b-dropdown-item>
|
||||
<o-icon icon="delete" />
|
||||
</o-dropdown-item>
|
||||
|
||||
<hr
|
||||
role="presentation"
|
||||
class="dropdown-divider"
|
||||
aria-role="menuitem"
|
||||
v-if="
|
||||
currentActor.id === post.author.id ||
|
||||
currentActor?.id === post?.author?.id ||
|
||||
isCurrentActorAGroupModerator
|
||||
"
|
||||
/>
|
||||
<b-dropdown-item
|
||||
<o-dropdown-item
|
||||
aria-role="listitem"
|
||||
v-if="!post.draft"
|
||||
@click="triggerShare()"
|
||||
@@ -129,11 +131,11 @@
|
||||
>
|
||||
<span>
|
||||
{{ $t("Share this event") }}
|
||||
<b-icon icon="share" />
|
||||
<o-icon icon="share" />
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
</o-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
<o-dropdown-item
|
||||
aria-role="listitem"
|
||||
v-if="ableToReport"
|
||||
@click="isReportModalActive = true"
|
||||
@@ -141,20 +143,21 @@
|
||||
>
|
||||
<span>
|
||||
{{ $t("Report") }}
|
||||
<b-icon icon="flag" />
|
||||
<o-icon icon="flag" />
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<b-message
|
||||
<o-notification
|
||||
:title="$t('Members-only post')"
|
||||
class="mx-4"
|
||||
type="is-warning"
|
||||
variant="warning"
|
||||
:closable="false"
|
||||
v-if="
|
||||
!$apollo.loading &&
|
||||
!membershipsLoading &&
|
||||
!postLoading &&
|
||||
isInstanceModerator &&
|
||||
!isCurrentActorAGroupMember &&
|
||||
post.visibility === PostVisibility.PRIVATE
|
||||
@@ -165,15 +168,15 @@
|
||||
"This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator."
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
</o-notification>
|
||||
|
||||
<section
|
||||
v-html="post.body"
|
||||
dir="auto"
|
||||
class="content"
|
||||
class="px-1 prose lg:prose-xl prose-p:mt-6 dark:prose-invert"
|
||||
:lang="post.language"
|
||||
/>
|
||||
<section class="tags" dir="auto">
|
||||
<section class="flex gap-2 my-6" dir="auto">
|
||||
<router-link
|
||||
v-for="tag in post.tags"
|
||||
:key="tag.title"
|
||||
@@ -182,9 +185,9 @@
|
||||
<tag>{{ tag.title }}</tag>
|
||||
</router-link>
|
||||
</section>
|
||||
<b-modal
|
||||
<o-modal
|
||||
:close-button-aria-label="$t('Close')"
|
||||
:active.sync="isReportModalActive"
|
||||
v-model:active="isReportModalActive"
|
||||
has-modal-card
|
||||
ref="reportModal"
|
||||
>
|
||||
@@ -192,198 +195,228 @@
|
||||
:on-confirm="reportPost"
|
||||
:title="$t('Report this post')"
|
||||
:outside-domain="groupDomain"
|
||||
@close="$refs.reportModal.close()"
|
||||
@close="isReportModalActive = false"
|
||||
/>
|
||||
</b-modal>
|
||||
<b-modal
|
||||
:active.sync="isShareModalActive"
|
||||
</o-modal>
|
||||
<o-modal
|
||||
v-model:active="isShareModalActive"
|
||||
has-modal-card
|
||||
ref="shareModal"
|
||||
:close-button-aria-label="$t('Close')"
|
||||
>
|
||||
<share-post-modal :post="post" />
|
||||
</b-modal>
|
||||
</o-modal>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop } from "vue-property-decorator";
|
||||
import { mixins } from "vue-class-component";
|
||||
import GroupMixin from "@/mixins/group";
|
||||
import { ICurrentUserRole, PostVisibility } from "@/types/enums";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
<script lang="ts" setup>
|
||||
import { ICurrentUserRole, MemberRole, PostVisibility } from "@/types/enums";
|
||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import {
|
||||
CURRENT_ACTOR_CLIENT,
|
||||
PERSON_MEMBERSHIPS,
|
||||
PERSON_STATUS_GROUP,
|
||||
} from "../../graphql/actor";
|
||||
import { usernameWithDomain } from "../../types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import Tag from "../../components/Tag.vue";
|
||||
import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue";
|
||||
import ActorInline from "../../components/Account/ActorInline.vue";
|
||||
import { formatDistanceToNowStrict } from "date-fns";
|
||||
import { CURRENT_USER_CLIENT } from "@/graphql/user";
|
||||
import { ICurrentUser } from "@/types/current-user.model";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import SharePostModal from "../../components/Post/SharePostModal.vue";
|
||||
import { IReport } from "@/types/report.model";
|
||||
IGroup,
|
||||
IPerson,
|
||||
usernameWithDomain,
|
||||
displayName,
|
||||
} from "@/types/actor";
|
||||
import RouteName from "@/router/name";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
||||
import ActorInline from "@/components/Account/ActorInline.vue";
|
||||
import { formatDistanceToNowStrict, Locale } from "date-fns";
|
||||
import SharePostModal from "@/components/Post/SharePostModal.vue";
|
||||
import { CREATE_REPORT } from "@/graphql/report";
|
||||
import ReportModal from "../../components/Report/ReportModal.vue";
|
||||
import PostMixin from "../../mixins/post";
|
||||
import ReportModal from "@/components/Report/ReportModal.vue";
|
||||
import { useAnonymousReportsConfig } from "@/composition/apollo/config";
|
||||
import {
|
||||
useCurrentActorClient,
|
||||
usePersonStatusGroup,
|
||||
} from "@/composition/apollo/actor";
|
||||
import { useCurrentUserClient } from "@/composition/apollo/user";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { IPost } from "@/types/post.model";
|
||||
import { DELETE_POST, FETCH_POST } from "@/graphql/post";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCreateReport } from "@/composition/apollo/report";
|
||||
import Clock from "vue-material-design-icons/Clock.vue";
|
||||
import Lock from "vue-material-design-icons/Lock.vue";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
config: CONFIG,
|
||||
currentUser: CURRENT_USER_CLIENT,
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
memberships: {
|
||||
query: PERSON_MEMBERSHIPS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
id: this.currentActor.id,
|
||||
};
|
||||
const props = defineProps<{
|
||||
slug: string;
|
||||
}>();
|
||||
|
||||
const { anonymousReportsConfig } = useAnonymousReportsConfig();
|
||||
const { currentUser } = useCurrentUserClient();
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const { result: membershipsResult, loading: membershipsLoading } = useQuery<{
|
||||
person: Pick<IPerson, "memberships">;
|
||||
}>(
|
||||
PERSON_MEMBERSHIPS,
|
||||
() => ({ id: currentActor.value?.id }),
|
||||
() => ({ enabled: currentActor.value?.id !== undefined })
|
||||
);
|
||||
const memberships = computed(() => membershipsResult.value?.person.memberships);
|
||||
|
||||
const { result: postResult, loading: postLoading } = useQuery<{
|
||||
post: IPost;
|
||||
}>(FETCH_POST, () => ({ slug: props.slug }));
|
||||
|
||||
const post = computed(() => postResult.value?.post);
|
||||
|
||||
usePersonStatusGroup(usernameWithDomain(post.value?.attributedTo as IGroup));
|
||||
|
||||
useHead({
|
||||
title: computed(
|
||||
() => `${post.value?.title} - ${displayName(post.value?.attributedTo)}`
|
||||
),
|
||||
});
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
const isShareModalActive = ref(false);
|
||||
const isReportModalActive = ref(false);
|
||||
const reportModal = ref();
|
||||
|
||||
const isCurrentActorMember = computed((): boolean => {
|
||||
if (!post.value?.attributedTo || !memberships.value) return false;
|
||||
return memberships.value.elements
|
||||
.map(({ parent: { id } }) => id)
|
||||
.includes(post.value?.attributedTo.id);
|
||||
});
|
||||
|
||||
const isInstanceModerator = computed((): boolean => {
|
||||
return (
|
||||
currentUser.value?.role !== undefined &&
|
||||
[ICurrentUserRole.ADMINISTRATOR, ICurrentUserRole.MODERATOR].includes(
|
||||
currentUser.value?.role
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const ableToReport = computed((): boolean => {
|
||||
return (
|
||||
currentActor.value?.id != undefined ||
|
||||
anonymousReportsConfig.value?.allowed === true
|
||||
);
|
||||
});
|
||||
|
||||
const triggerShare = (): void => {
|
||||
if (navigator.share) {
|
||||
navigator
|
||||
.share({
|
||||
title: post.value?.title,
|
||||
url: post.value?.url,
|
||||
})
|
||||
.then(() => console.log("Successful share"))
|
||||
.catch((error: any) => console.log("Error sharing", error));
|
||||
} else {
|
||||
isShareModalActive.value = true;
|
||||
// send popup
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: createReportMutation,
|
||||
onDone: onCreateReportDone,
|
||||
onError: onCreateReportError,
|
||||
} = useCreateReport();
|
||||
|
||||
onCreateReportDone(() => {
|
||||
isReportModalActive.value = false;
|
||||
reportModal.value.close();
|
||||
const postTitle = post.value?.title;
|
||||
notifier?.success(t("Post {eventTitle} reported", { postTitle }));
|
||||
});
|
||||
|
||||
onCreateReportError((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const reportPost = async (content: string, forward: boolean): Promise<void> => {
|
||||
createReportMutation({
|
||||
// postId: post.value?.id,
|
||||
reportedId: post.value?.attributedTo?.id as string,
|
||||
content,
|
||||
forward,
|
||||
});
|
||||
};
|
||||
const groupDomain = computed((): string | undefined | null => {
|
||||
return post.value?.attributedTo?.domain;
|
||||
});
|
||||
|
||||
const dateFnsLocale = inject<Locale>("dateFnsLocale");
|
||||
|
||||
const isCurrentActorAGroupModerator = computed((): boolean => {
|
||||
return hasCurrentActorThisRole([
|
||||
MemberRole.MODERATOR,
|
||||
MemberRole.ADMINISTRATOR,
|
||||
]);
|
||||
});
|
||||
|
||||
const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
|
||||
const roles = Array.isArray(givenRole)
|
||||
? givenRole
|
||||
: ([givenRole] as MemberRole[]);
|
||||
return (
|
||||
(memberships.value?.total ?? 0) > 0 &&
|
||||
roles.includes(memberships.value?.elements[0].role as MemberRole)
|
||||
);
|
||||
};
|
||||
|
||||
const isCurrentActorAGroupMember = computed((): boolean => {
|
||||
return hasCurrentActorThisRole([
|
||||
MemberRole.MODERATOR,
|
||||
MemberRole.ADMINISTRATOR,
|
||||
MemberRole.MEMBER,
|
||||
]);
|
||||
});
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const dialog = inject<Dialog>("dialog");
|
||||
|
||||
const openDeletePostModal = async (): Promise<void> => {
|
||||
dialog?.confirm({
|
||||
type: "danger",
|
||||
title: t("Delete post"),
|
||||
message: t(
|
||||
"Are you sure you want to delete this post? This action cannot be reverted."
|
||||
),
|
||||
onConfirm: () =>
|
||||
deletePost({
|
||||
id: post.value?.id,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
mutate: deletePost,
|
||||
onDone: onDeletePostDone,
|
||||
onError: onDeletePostError,
|
||||
} = useMutation(DELETE_POST);
|
||||
|
||||
onDeletePostDone(({ data }) => {
|
||||
if (data && post.value?.attributedTo) {
|
||||
router.push({
|
||||
name: RouteName.POSTS,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(post.value?.attributedTo),
|
||||
},
|
||||
update: (data) => data.person.memberships.elements,
|
||||
skip() {
|
||||
return !this.currentActor || !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
person: {
|
||||
query: PERSON_STATUS_GROUP,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
id: this.currentActor.id,
|
||||
group: usernameWithDomain(this.post.attributedTo),
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return (
|
||||
!this.currentActor ||
|
||||
!this.currentActor.id ||
|
||||
!this.post?.attributedTo
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Tag,
|
||||
LazyImageWrapper,
|
||||
ActorInline,
|
||||
SharePostModal,
|
||||
ReportModal,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
title: this.post ? this.post.title : "",
|
||||
// all titles will be injected into this template
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
titleTemplate: this.post ? "%s | Mobilizon" : "Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Post extends mixins(GroupMixin, PostMixin) {
|
||||
@Prop({ required: true, type: String }) slug!: string;
|
||||
|
||||
memberships!: IMember[];
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
currentUser!: ICurrentUser;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
formatDistanceToNowStrict = formatDistanceToNowStrict;
|
||||
|
||||
PostVisibility = PostVisibility;
|
||||
|
||||
isShareModalActive = false;
|
||||
|
||||
isReportModalActive = false;
|
||||
|
||||
get isCurrentActorMember(): boolean {
|
||||
if (!this.post.attributedTo || !this.memberships) return false;
|
||||
return this.memberships
|
||||
.map(({ parent: { id } }) => id)
|
||||
.includes(this.post.attributedTo.id);
|
||||
});
|
||||
}
|
||||
|
||||
get isInstanceModerator(): boolean {
|
||||
return [
|
||||
ICurrentUserRole.ADMINISTRATOR,
|
||||
ICurrentUserRole.MODERATOR,
|
||||
].includes(this.currentUser.role);
|
||||
}
|
||||
|
||||
get ableToReport(): boolean {
|
||||
return (
|
||||
this.config &&
|
||||
(this.currentActor.id != null || this.config.anonymous.reports.allowed)
|
||||
);
|
||||
}
|
||||
|
||||
triggerShare(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-start
|
||||
if (navigator.share) {
|
||||
navigator
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
.share({
|
||||
title: this.post.title,
|
||||
url: this.post.url,
|
||||
})
|
||||
.then(() => console.log("Successful share"))
|
||||
.catch((error: any) => console.log("Error sharing", error));
|
||||
} else {
|
||||
this.isShareModalActive = true;
|
||||
// send popup
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-end
|
||||
}
|
||||
|
||||
async reportPost(content: string, forward: boolean): Promise<void> {
|
||||
this.isReportModalActive = false;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.$refs.reportModal.close();
|
||||
const postTitle = this.post.title;
|
||||
|
||||
try {
|
||||
await this.$apollo.mutate<IReport>({
|
||||
mutation: CREATE_REPORT,
|
||||
variables: {
|
||||
postId: this.post.id,
|
||||
reportedId: this.post.attributedTo?.id,
|
||||
content,
|
||||
forward,
|
||||
},
|
||||
});
|
||||
this.$notifier.success(
|
||||
this.$t("Post {eventTitle} reported", { postTitle }) as string
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
get groupDomain(): string | undefined | null {
|
||||
return this.post.attributedTo?.domain;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/_mixins" as *;
|
||||
article.post {
|
||||
background: $white !important;
|
||||
// background: $white !important;
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -463,36 +496,6 @@ article.post {
|
||||
}
|
||||
}
|
||||
|
||||
& > section {
|
||||
margin: 0 2rem;
|
||||
|
||||
&.content {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
&.tags {
|
||||
padding-bottom: 5rem;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
span {
|
||||
&.tag {
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
margin: 0 auto;
|
||||
|
||||
a.dropdown-item,
|
||||
.dropdown .dropdown-menu .has-link a,
|
||||
button.dropdown-item {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
@include padding-right(1rem);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user