feat(reports): improve reportview and allow removing content + resolve report automatically

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2023-08-31 14:37:54 +02:00
parent f2ac3e2e5d
commit b105c508c0
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
7 changed files with 230 additions and 94 deletions

View file

@ -4,7 +4,7 @@
:class="{ :class="{
reply: comment.inReplyToComment, reply: comment.inReplyToComment,
'bg-mbz-purple-50 dark:bg-mbz-purple-500': comment.isAnnouncement, 'bg-mbz-purple-50 dark:bg-mbz-purple-500': comment.isAnnouncement,
'bg-mbz-bluegreen-50 dark:bg-mbz-bluegreen-600': commentSelected, '!bg-mbz-bluegreen-50 dark:!bg-mbz-bluegreen-600': commentSelected,
'shadow-none': !rootComment, 'shadow-none': !rootComment,
}" }"
> >
@ -62,6 +62,7 @@
class="cursor-pointer flex hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1" class="cursor-pointer flex hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
v-if=" v-if="
currentActor?.id && currentActor?.id &&
!readOnly &&
event.options.commentModeration !== CommentModeration.CLOSED && event.options.commentModeration !== CommentModeration.CLOSED &&
!comment.deletedAt !comment.deletedAt
" "
@ -70,7 +71,7 @@
<Reply /> <Reply />
<span>{{ t("Reply") }}</span> <span>{{ t("Reply") }}</span>
</button> </button>
<o-dropdown aria-role="list"> <o-dropdown aria-role="list" v-show="!readOnly">
<template #trigger> <template #trigger>
<button <button
class="cursor-pointer flex hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1" class="cursor-pointer flex hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
@ -221,7 +222,7 @@ import {
ref, ref,
nextTick, nextTick,
} from "vue"; } from "vue";
import { useRoute } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue"; import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import Delete from "vue-material-design-icons/Delete.vue"; import Delete from "vue-material-design-icons/Delete.vue";
@ -235,6 +236,9 @@ import ReportModal from "@/components/Report/ReportModal.vue";
import { useCreateReport } from "@/composition/apollo/report"; import { useCreateReport } from "@/composition/apollo/report";
import { Snackbar } from "@/plugins/snackbar"; import { Snackbar } from "@/plugins/snackbar";
import { useProgrammatic } from "@oruga-ui/oruga-next"; import { useProgrammatic } from "@oruga-ui/oruga-next";
import RouteName from "@/router/name";
const router = useRouter();
const Editor = defineAsyncComponent( const Editor = defineAsyncComponent(
() => import("@/components/TextEditor.vue") () => import("@/components/TextEditor.vue")
@ -246,10 +250,13 @@ const props = withDefaults(
event: IEvent; event: IEvent;
currentActor: IPerson; currentActor: IPerson;
rootComment?: boolean; rootComment?: boolean;
readOnly: boolean;
}>(), }>(),
{ rootComment: true } { rootComment: true, readOnly: false }
); );
const event = computed(() => props.event);
const emit = defineEmits<{ const emit = defineEmits<{
(e: "create-comment", comment: IComment): void; (e: "create-comment", comment: IComment): void;
(e: "delete-comment", comment: IComment): void; (e: "delete-comment", comment: IComment): void;
@ -319,7 +326,12 @@ const commentId = computed((): string => {
const commentURL = computed((): string => { const commentURL = computed((): string => {
if (!props.comment.local && props.comment.url) return props.comment.url; if (!props.comment.local && props.comment.url) return props.comment.url;
return `#${commentId.value}`; return (
router.resolve({
name: RouteName.EVENT,
params: { uuid: event.value.uuid },
}).href + `#${commentId.value}`
);
}); });
const reportModal = (): void => { const reportModal = (): void => {
@ -352,7 +364,6 @@ const reportComment = async (
): Promise<void> => { ): Promise<void> => {
if (!props.comment.actor) return; if (!props.comment.actor) return;
createReportMutation({ createReportMutation({
eventsIds: [props.event.id ?? ""],
reportedId: props.comment.actor?.id ?? "", reportedId: props.comment.actor?.id ?? "",
commentsIds: [props.comment.id ?? ""], commentsIds: [props.comment.id ?? ""],
content, content,

View file

@ -69,6 +69,14 @@ const REPORT_FRAGMENT = gql`
actor { actor {
...ActorFragment ...ActorFragment
} }
updatedAt
deletedAt
uuid
event {
id
uuid
title
}
} }
notes { notes {
id id

View file

@ -78,8 +78,6 @@
"Date parameters": "Date parameters", "Date parameters": "Date parameters",
"Date": "Date", "Date": "Date",
"Default": "Default", "Default": "Default",
"Delete Comment": "Delete Comment",
"Delete Event": "Delete Event",
"Delete account": "Delete account", "Delete account": "Delete account",
"Delete event": "Delete event", "Delete event": "Delete event",
"Delete everything": "Delete everything", "Delete everything": "Delete everything",
@ -1582,5 +1580,17 @@
"This application will be allowed to list your suggested group events": "This application will be allowed to list your suggested group events", "This application will be allowed to list your suggested group events": "This application will be allowed to list your suggested group events",
"{profile} joined the the event {event}.": "{profile} joined the the event {event}.", "{profile} joined the the event {event}.": "{profile} joined the the event {event}.",
"You joined the event {event}.": "You joined the event {event}.", "You joined the event {event}.": "You joined the event {event}.",
"An anonymous profile joined the event {event}.": "An anonymous profile joined the event {event}." "An anonymous profile joined the event {event}.": "An anonymous profile joined the event {event}.",
"Delete event and resolve report": "Delete event and resolve report",
"No content found": "No content found",
"Maybe the content was removed by the author or a moderator": "Maybe the content was removed by the author or a moderator",
"This will also resolve the report.": "This will also resolve the report.",
"Are you sure you want to <b>delete</b> this event? <b>This action cannot be undone</b>. You may want to engage the discussion with the event creator and ask them to edit their event instead.": "Are you sure you want to <b>delete</b> this event? <b>This action cannot be undone</b>. You may want to engage the discussion with the event creator and ask them to edit their event instead.",
"Are you sure you want to <b>delete</b> this comment? <b>This action cannot be undone</b>.": "Are you sure you want to <b>delete</b> this comment? <b>This action cannot be undone</b>.",
"Delete comment and resolve report": "Delete comment and resolve report",
"Delete comment": "Delete comment",
"Event deleted and report resolved": "Event deleted and report resolved",
"Event deleted": "Event deleted",
"Comment deleted and report resolved": "Comment deleted and report resolved",
"Comment under event {eventTitle}": "Comment under event {eventTitle}"
} }

View file

@ -1578,5 +1578,17 @@
"This application will be allowed to list your suggested group events": "Cetta application sera autorisée à lister les événements de vos groupes qui vous sont suggérés", "This application will be allowed to list your suggested group events": "Cetta application sera autorisée à lister les événements de vos groupes qui vous sont suggérés",
"{profile} joined the the event {event}.": "{profile} a rejoint l'événement {event}.", "{profile} joined the the event {event}.": "{profile} a rejoint l'événement {event}.",
"You joined the event {event}.": "Vous avez rejoint l'événement {event}.", "You joined the event {event}.": "Vous avez rejoint l'événement {event}.",
"An anonymous profile joined the event {event}.": "Un profil anonyme a rejoint l'événement {event}." "An anonymous profile joined the event {event}.": "Un profil anonyme a rejoint l'événement {event}.",
"Delete event and resolve report": "Supprimer l'événement et résoudre le signalement",
"No content found": "Aucun contenu trouvé",
"Maybe the content was removed by the author or a moderator": "Peut-être que le contenu a été supprimé par l'auteur·ice ou un·e modérateur·ice",
"This will also resolve the report.": "Cela résoudra également le signalement.",
"Are you sure you want to <b>delete</b> this event? <b>This action cannot be undone</b>. You may want to engage the discussion with the event creator and ask them to edit their event instead.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> cet événement ? Cette action n'est pas réversible. Vous voulez peut-être engager la discussion avec le créateur de l'événement et lui demander de modifier son événement à la place.",
"Are you sure you want to <b>delete</b> this comment? <b>This action cannot be undone</b>.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> ce commentaire ? <b>Cette action ne peut pas être annulée.</b>",
"Delete comment and resolve report": "Supprimer le commentaire et résoudre le signalement",
"Delete comment": "Supprimer le commentaire",
"Event deleted and report resolved": "Événement supprimé et signalement résolu",
"Event deleted": "Événement supprimé",
"Comment deleted and report resolved": "Commentaire supprimé et signalement résolu",
"Comment under event {eventTitle}": "Commentaire sous l'événement {eventTitle}"
} }

View file

@ -150,26 +150,6 @@
<span v-else>{{ t("Unknown") }}</span> <span v-else>{{ t("Unknown") }}</span>
</td> </td>
</tr> </tr>
<!-- <tr v-if="report.events && report.comments.length > 0">
<td>{{ t("Events") }}</td>
<td class="flex gap-2 items-center">
<router-link
class="underline"
:to="{
name: RouteName.EVENT,
params: { uuid: report.events.uuid },
}"
>
{{ report.event.title }}
</router-link>
<o-button
variant="danger"
@click="confirmEventDelete()"
icon-left="delete"
>{{ t("Delete") }}</o-button
>
</td>
</tr> -->
</tbody> </tbody>
</table> </table>
</section> </section>
@ -213,16 +193,20 @@
" "
> >
<h2 class="mb-1">{{ t("Reported content") }}</h2> <h2 class="mb-1">{{ t("Reported content") }}</h2>
<div v-for="event in report.events" :key="event.id"> <ul>
<li v-for="event in report.events" :key="event.id">
<EventCard :event="event" mode="row" class="my-2 max-w-4xl" /> <EventCard :event="event" mode="row" class="my-2 max-w-4xl" />
<o-button <o-button
variant="danger" variant="danger"
@click="confirmEventDelete(event)" @click="confirmEventDelete(event)"
icon-left="delete" icon-left="delete"
size="small" ><template v-if="isOnlyReportedContent">{{
>{{ t("Delete") }}</o-button t("Delete event and resolve report")
}}</template
><template v-else>{{ t("Delete event") }}</template></o-button
> >
</div> </li>
</ul>
</section> </section>
<section <section
@ -232,41 +216,55 @@
<h2 class="mb-1">{{ t("Reported content") }}</h2> <h2 class="mb-1">{{ t("Reported content") }}</h2>
<ul v-for="comment in report.comments" :key="comment.id"> <ul v-for="comment in report.comments" :key="comment.id">
<li> <li>
<div class="" v-if="comment"> <i18n-t keypath="Comment under event {eventTitle}" tag="p">
<article> <template #eventTitle>
<div class="flex gap-1"> <router-link
<figure class="" v-if="comment.actor?.avatar"> :to="{
<img name: RouteName.EVENT,
alt="" params: { uuid: comment.event?.uuid },
:src="comment.actor.avatar?.url" }"
class="rounded-full" >
width="36" <b>{{ comment.event?.title }}</b>
height="36" </router-link>
</template>
</i18n-t>
<EventComment
:root-comment="true"
:comment="comment"
:event="comment.event as IEvent"
:current-actor="currentActor as IPerson"
:readOnly="true"
/> />
</figure>
<AccountCircle v-else :size="36" />
<div>
<div v-if="comment.actor">
<p>{{ comment.actor.name }}</p>
<p>@{{ comment.actor.preferredUsername }}</p>
</div>
<span v-else>{{ t("Unknown actor") }}</span>
</div>
</div>
<div class="prose dark:prose-invert" v-html="comment.text" />
<o-button <o-button
v-if="!comment.deletedAt"
variant="danger" variant="danger"
@click="confirmCommentDelete(comment)" @click="confirmCommentDelete(comment)"
icon-left="delete" icon-left="delete"
size="small" ><template v-if="isOnlyReportedContent">{{
>{{ t("Delete") }}</o-button t("Delete comment and resolve report")
}}</template
><template v-else>{{ t("Delete comment") }}</template></o-button
> >
</article>
</div>
</li> </li>
</ul> </ul>
</section> </section>
<section
class="bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3"
v-if="
report.events &&
report.events?.length === 0 &&
report.comments.length === 0
"
>
<EmptyContent inline center icon="alert-circle">
{{ t("No content found") }}
<template #desc>
{{ t("Maybe the content was removed by the author or a moderator") }}
</template>
</EmptyContent>
</section>
<section class="bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3"> <section class="bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3">
<h2 class="mb-1">{{ t("Notes") }}</h2> <h2 class="mb-1">{{ t("Notes") }}</h2>
<div <div
@ -321,7 +319,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from "@/graphql/report"; import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from "@/graphql/report";
import { IReport, IReportNote } from "@/types/report.model"; import { IReport, IReportNote } from "@/types/report.model";
import { displayNameAndUsername, usernameWithDomain } from "@/types/actor"; import {
IPerson,
displayNameAndUsername,
usernameWithDomain,
} from "@/types/actor";
import { DELETE_EVENT } from "@/graphql/event"; import { DELETE_EVENT } from "@/graphql/event";
import uniq from "lodash/uniq"; import uniq from "lodash/uniq";
import { nl2br } from "@/utils/html"; import { nl2br } from "@/utils/html";
@ -344,6 +346,8 @@ import { Notifier } from "@/plugins/notifier";
import EventCard from "@/components/Event/EventCard.vue"; import EventCard from "@/components/Event/EventCard.vue";
import { useFeatures } from "@/composition/apollo/config"; import { useFeatures } from "@/composition/apollo/config";
import { IEvent } from "@/types/event.model"; import { IEvent } from "@/types/event.model";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
import EventComment from "@/components/Comment/EventComment.vue";
const router = useRouter(); const router = useRouter();
@ -381,6 +385,14 @@ const errors = ref<string[]>([]);
const noteContent = ref(""); const noteContent = ref("");
const reportedContent = computed(() => {
return [...(report.value?.events ?? []), ...(report.value?.comments ?? [])];
});
const isOnlyReportedContent = computed(
() => reportedContent.value.length === 1
);
const { const {
mutate: createReportNoteMutation, mutate: createReportNoteMutation,
onDone: createReportNoteMutationDone, onDone: createReportNoteMutationDone,
@ -426,13 +438,23 @@ createReportNoteMutationError((error) => {
const dialog = inject<Dialog>("dialog"); const dialog = inject<Dialog>("dialog");
const addResolveReportPart = computed(() => {
if (isOnlyReportedContent.value) {
return "<p>" + t("This will also resolve the report.") + "</p>";
}
return "";
});
const confirmEventDelete = (event: IEvent): void => { const confirmEventDelete = (event: IEvent): void => {
dialog?.confirm({ dialog?.confirm({
title: t("Deleting event"), title: t("Deleting event"),
message: t( message:
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead." t(
), "Are you sure you want to <b>delete</b> this event? <b>This action cannot be undone</b>. You may want to engage the discussion with the event creator and ask them to edit their event instead."
confirmText: t("Delete Event"), ) + addResolveReportPart.value,
confirmText: isOnlyReportedContent.value
? t("Delete event and resolve report")
: t("Delete event"),
variant: "danger", variant: "danger",
hasIcon: true, hasIcon: true,
onConfirm: () => deleteEvent(event), onConfirm: () => deleteEvent(event),
@ -442,10 +464,13 @@ const confirmEventDelete = (event: IEvent): void => {
const confirmCommentDelete = (comment: IComment): void => { const confirmCommentDelete = (comment: IComment): void => {
dialog?.confirm({ dialog?.confirm({
title: t("Deleting comment"), title: t("Deleting comment"),
message: t( message:
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone." t(
), "Are you sure you want to <b>delete</b> this comment? <b>This action cannot be undone</b>."
confirmText: t("Delete Comment"), ) + addResolveReportPart.value,
confirmText: isOnlyReportedContent.value
? t("Delete comment and resolve report")
: t("Delete comment"),
variant: "danger", variant: "danger",
hasIcon: true, hasIcon: true,
onConfirm: () => deleteCommentMutation({ commentId: comment.id }), onConfirm: () => deleteCommentMutation({ commentId: comment.id }),
@ -456,15 +481,46 @@ const {
mutate: deleteEventMutation, mutate: deleteEventMutation,
onDone: deleteEventMutationDone, onDone: deleteEventMutationDone,
onError: deleteEventMutationError, onError: deleteEventMutationError,
} = useMutation<{ deleteEvent: { id: string } }>(DELETE_EVENT); } = useMutation<{ deleteEvent: { id: string } }>(DELETE_EVENT, () => ({
update: (
deleteEventMutationDone((result) => { store: ApolloCache<{ deleteEvent: { id: string } }>,
const eventTitle = result?.context?.eventTitle; { data }: FetchResult
notifier?.success( ) => {
t("Event {eventTitle} deleted", { if (data == null) return;
eventTitle, const reportCachedData = store.readQuery<{ report: IReport }>({
}) query: REPORT,
variables: { id: report.value?.id },
});
if (reportCachedData == null) return;
const { report: cachedReport } = reportCachedData;
if (cachedReport === null) {
console.error(
"Cannot update report events cache, because of null value."
); );
return;
}
const updatedReport = {
...cachedReport,
events: cachedReport.events?.filter(
(cachedEvent) => cachedEvent.id !== data.deleteEvent.id
),
};
store.writeQuery({
query: REPORT,
variables: { id: report.value?.id },
data: { report: updatedReport },
});
},
}));
deleteEventMutationDone(async () => {
if (reportedContent.value.length === 0) {
await updateReport(ReportStatusEnum.RESOLVED);
notifier?.success(t("Event deleted and report resolved"));
} else {
notifier?.success(t("Event deleted"));
}
}); });
deleteEventMutationError((error) => { deleteEventMutationError((error) => {
@ -484,10 +540,46 @@ const {
mutate: deleteCommentMutation, mutate: deleteCommentMutation,
onDone: deleteCommentMutationDone, onDone: deleteCommentMutationDone,
onError: deleteCommentMutationError, onError: deleteCommentMutationError,
} = useMutation<{ deleteComment: { id: string } }>(DELETE_COMMENT); } = useMutation<{ deleteComment: { id: string } }>(DELETE_COMMENT, () => ({
update: (
store: ApolloCache<{ deleteComment: { id: string } }>,
{ data }: FetchResult
) => {
if (data == null) return;
const reportCachedData = store.readQuery<{ report: IReport }>({
query: REPORT,
variables: { id: report.value?.id },
});
if (reportCachedData == null) return;
const { report: cachedReport } = reportCachedData;
if (cachedReport === null) {
console.error(
"Cannot update report comments cache, because of null value."
);
return;
}
const updatedReport = {
...cachedReport,
comments: cachedReport.comments.filter(
(cachedComment) => cachedComment.id !== data.deleteComment.id
),
};
deleteCommentMutationDone(() => { store.writeQuery({
notifier?.success(t("Comment deleted") as string); query: REPORT,
variables: { id: report.value?.id },
data: { report: updatedReport },
});
},
}));
deleteCommentMutationDone(async () => {
if (reportedContent.value.length === 0) {
await updateReport(ReportStatusEnum.RESOLVED);
notifier?.success(t("Comment deleted and report resolved"));
} else {
notifier?.success(t("Comment deleted"));
}
}); });
deleteCommentMutationError((error) => { deleteCommentMutationError((error) => {
@ -534,8 +626,8 @@ const {
}, },
})); }));
onUpdateReportMutation(() => { onUpdateReportMutation(async () => {
router.push({ name: RouteName.REPORTS }); await router.push({ name: RouteName.REPORTS });
}); });
onUpdateReportError((error) => { onUpdateReportError((error) => {

View file

@ -34,7 +34,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
{:error, :event_not_found} -> nil {:error, :event_not_found} -> nil
end end
end) end)
|> Enum.filter(& &1) |> Enum.filter(fn event ->
is_struct(event) and
Enum.member?([event.organizer_actor_id, event.attributed_to_id], reported_actor.id)
end)
comments = comments =
Discussions.list_comments_by_actor_and_ids( Discussions.list_comments_by_actor_and_ids(

View file

@ -67,7 +67,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
{:ok, _, %Report{} = report} -> {:ok, _, %Report{} = report} ->
{:ok, report} {:ok, report}
error -> _error ->
{:error, dgettext("errors", "Error while saving report")} {:error, dgettext("errors", "Error while saving report")}
end end
end end