2022-08-26 16:08:58 +02:00
< template >
< breadcrumbs -nav
v - if = "report"
: links = " [
{
name : RouteName . MODERATION ,
text : t ( 'Moderation' ) ,
} ,
{
name : RouteName . REPORTS ,
text : t ( 'Reports' ) ,
} ,
{
name : RouteName . REPORT ,
params : { id : report . id } ,
text : t ( 'Report #{reportNumber}' , { reportNumber : report . id } ) ,
} ,
] "
/ >
< o -notification
title = "Error"
variant = "danger"
v - for = "error in errors"
: key = "error"
>
{ { error } }
< / o - n o t i f i c a t i o n >
< div class = "container mx-auto" v-if ="report" >
< div class = "flex flex-wrap gap-2 my-2" >
< o -button
v - if = "report.status !== ReportStatusEnum.RESOLVED"
@ click = "updateReport(ReportStatusEnum.RESOLVED)"
variant = "primary"
> { { t ( "Mark as resolved" ) } } < / o - b u t t o n
>
< o -button
v - if = "report.status !== ReportStatusEnum.OPEN"
@ click = "updateReport(ReportStatusEnum.OPEN)"
variant = "success"
> { { t ( "Reopen" ) } } < / o - b u t t o n
>
< o -button
v - if = "report.status !== ReportStatusEnum.CLOSED"
@ click = "updateReport(ReportStatusEnum.CLOSED)"
variant = "danger"
> { { t ( "Close" ) } } < / o - b u t t o n
>
< / div >
< section class = "w-full" >
< table class = "table w-full" >
< tbody >
< tr v-if ="report.reported.type === ActorType.GROUP" >
< td > { { t ( "Reported group" ) } } < / td >
< td >
< router -link
: to = " {
name : RouteName . ADMIN _GROUP _PROFILE ,
params : { id : report . reported . id } ,
} "
>
< img
v - if = "report.reported.avatar"
class = "image"
: src = "report.reported.avatar.url"
alt = ""
/ >
{ { displayNameAndUsername ( report . reported ) } }
< / r o u t e r - l i n k >
< / td >
< / tr >
< tr v-else >
< td >
{ { t ( "Reported identity" ) } }
< / td >
< td >
< router -link
: to = " {
name : RouteName . ADMIN _PROFILE ,
params : { id : report . reported . id } ,
} "
>
< img
v - if = "report.reported.avatar"
class = "image"
: src = "report.reported.avatar.url"
alt = ""
/ >
{ { displayNameAndUsername ( report . reported ) } }
< / r o u t e r - l i n k >
< / td >
< / tr >
< tr >
< td > { { t ( "Reported by" ) } } < / td >
< td v-if ="report.reporter.type === ActorType.APPLICATION" >
{ { report . reporter . domain } }
< / td >
< td v-else >
< router -link
: to = " {
name : RouteName . ADMIN _PROFILE ,
params : { id : report . reporter . id } ,
} "
>
< img
v - if = "report.reporter.avatar"
class = "image"
: src = "report.reporter.avatar.url"
alt = ""
/ >
{ { displayNameAndUsername ( report . reporter ) } }
< / r o u t e r - l i n k >
< / td >
< / tr >
< tr >
< td > { { t ( "Reported" ) } } < / td >
< td > { { formatDateTimeString ( report . insertedAt ) } } < / td >
< / tr >
< tr v-if ="report.updatedAt !== report.insertedAt" >
< td > { { t ( "Updated" ) } } < / td >
< td > { { formatDateTimeString ( report . updatedAt ) } } < / td >
< / tr >
< tr >
< td > { { t ( "Status" ) } } < / td >
< td >
< span v-if ="report.status === ReportStatusEnum.OPEN" > {{
t ( "Open" )
} } < / span >
< span v -else -if = " report.status = = = ReportStatusEnum.CLOSED " >
{ { t ( "Closed" ) } }
< / span >
< span v -else -if = " report.status = = = ReportStatusEnum.RESOLVED " >
{ { t ( "Resolved" ) } }
< / span >
< span v-else > {{ t ( " Unknown " ) }} < / span >
< / td >
< / tr >
< tr v-if ="report.event && report.comments.length > 0" >
< td > { { t ( "Event" ) } } < / td >
2022-10-25 19:01:43 +02:00
< td class = "flex gap-2 items-center" >
2022-08-26 16:08:58 +02:00
< router -link
2022-10-25 19:01:43 +02:00
class = "underline"
2022-08-26 16:08:58 +02:00
: to = " {
name : RouteName . EVENT ,
params : { uuid : report . event . uuid } ,
} "
>
{ { report . event . title } }
< / r o u t e r - l i n k >
2022-10-25 19:01:43 +02:00
< o -button
variant = "danger"
@ click = "confirmEventDelete()"
icon - left = "delete"
> { { t ( "Delete" ) } } < / o - b u t t o n
>
2022-08-26 16:08:58 +02:00
< / td >
< / tr >
< / tbody >
< / table >
< / section >
2022-10-25 19:01:43 +02:00
< section class = "bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3" >
< h2 class = "mb-1" > { { t ( "Report reason" ) } } < / h2 >
< div class = "" >
2022-08-26 16:08:58 +02:00
< div class = "flex gap-1" >
< figure class = "" v-if ="report.reported.avatar" >
< img
alt = ""
: src = "report.reported.avatar.url"
class = "rounded-full"
2022-10-25 19:01:43 +02:00
width = "36"
height = "36"
2022-08-26 16:08:58 +02:00
/ >
< / figure >
2022-10-25 19:01:43 +02:00
< AccountCircle v -else :size ="36" / >
2022-08-26 16:08:58 +02:00
< div class = "" >
< p class = "" v-if ="report.reported.name" >
{ { report . reported . name } }
< / p >
< p class = "" > @ { { usernameWithDomain ( report . reported ) } } < / p >
< / div >
< / div >
< div
class = "prose dark:prose-invert"
v - if = "report.content"
v - html = "nl2br(report.content)"
/ >
< p v-else > {{ t ( " No comment " ) }} < / p >
< / div >
< / section >
2022-10-25 19:01:43 +02:00
< section
class = "bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3"
v - if = "report.event && report.comments.length === 0"
>
< h2 class = "mb-1" > { { t ( "Reported content" ) } } < / h2 >
< EventCard :event ="report.event" mode = "row" class = "my-2 max-w-4xl" / >
2022-08-26 16:08:58 +02:00
< o -button
variant = "danger"
@ click = "confirmEventDelete()"
icon - left = "delete"
size = "small"
> { { t ( "Delete" ) } } < / o - b u t t o n
>
< / section >
2022-10-25 19:01:43 +02:00
< section
class = "bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3"
v - if = "report.comments.length > 0"
>
< h2 class = "mb-1" > { { t ( "Reported content" ) } } < / h2 >
2022-08-26 16:08:58 +02:00
< ul v-for ="comment in report.comments" :key="comment.id" >
< li >
< div class = "" v-if ="comment" >
2022-10-25 19:01:43 +02:00
< article >
< div class = "flex gap-1" >
< figure class = "" v-if ="comment.actor?.avatar" >
2022-08-26 16:08:58 +02:00
< img
alt = ""
2022-10-25 19:01:43 +02:00
: src = "comment.actor.avatar?.url"
class = "rounded-full"
width = "36"
height = "36"
2022-08-26 16:08:58 +02:00
/ >
< / figure >
2022-10-25 19:01:43 +02:00
< AccountCircle v -else :size ="36" / >
< div >
< div v-if ="comment.actor" >
< p > { { comment . actor . name } } < / p >
< p > @ { { comment . actor . preferredUsername } } < / p >
< / div >
2022-08-26 16:08:58 +02:00
< span v-else > {{ t ( " Unknown actor " ) }} < / span >
< / div >
< / div >
2022-10-25 19:01:43 +02:00
< div class = "prose dark:prose-invert" v -html = " comment.text " / >
< o -button
variant = "danger"
@ click = "confirmCommentDelete(comment)"
icon - left = "delete"
size = "small"
> { { t ( "Delete" ) } } < / o - b u t t o n
>
2022-08-26 16:08:58 +02:00
< / article >
< / div >
< / li >
< / ul >
2022-10-25 19:01:43 +02:00
< / section >
2022-08-26 16:08:58 +02:00
2022-10-25 19:01:43 +02:00
< section class = "bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3" >
< h2 class = "mb-1" > { { t ( "Notes" ) } } < / h2 >
2022-08-26 16:08:58 +02:00
< div
2022-10-25 19:01:43 +02:00
class = ""
2022-08-26 16:08:58 +02:00
v - for = "note in report.notes"
: id = "`note-${note.id}`"
: key = "note.id"
>
< p > { { note . content } } < / p >
< router -link
: to = " {
name : RouteName . ADMIN _PROFILE ,
params : { id : note . moderator . id } ,
} "
>
< img
alt = ""
class = "rounded-full"
: src = "note.moderator.avatar.url"
v - if = "note.moderator.avatar"
/ >
@ { { note . moderator . preferredUsername } }
< / r o u t e r - l i n k >
< br / >
< small >
< a :href ="`#note-${note.id}`" v-if ="note.insertedAt" >
{ { formatDateTimeString ( note . insertedAt ) } }
< / a >
< / small >
< / div >
< form
@ submit = "
createReportNoteMutation ( {
reportId : report ? . id ,
content : noteContent ,
} )
"
>
< o -field : label = "t('New note')" label -for = " newNoteInput " >
< o -input
type = "textarea"
v - model = "noteContent"
id = "newNoteInput"
> < / o - i n p u t >
< / o - f i e l d >
< o -button class = "mt-2" type = "submit" > { { t ( "Add a note" ) } } < / o - b u t t o n >
< / form >
< / section >
< / div >
< / template >
< script lang = "ts" setup >
import { CREATE _REPORT _NOTE , REPORT , UPDATE _REPORT } from "@/graphql/report" ;
import { IReport , IReportNote } from "@/types/report.model" ;
import { displayNameAndUsername , usernameWithDomain } from "@/types/actor" ;
import { DELETE _EVENT } from "@/graphql/event" ;
import uniq from "lodash/uniq" ;
import { nl2br } from "@/utils/html" ;
import { DELETE _COMMENT } from "@/graphql/comment" ;
import { IComment } from "@/types/comment.model" ;
import { ActorType , ReportStatusEnum } from "@/types/enums" ;
import RouteName from "@/router/name" ;
import { GraphQLError } from "graphql" ;
import { ApolloCache , FetchResult } from "@apollo/client/core" ;
import { useMutation , useQuery } from "@vue/apollo-composable" ;
import { useCurrentActorClient } from "@/composition/apollo/actor" ;
import { useHead } from "@vueuse/head" ;
import { useI18n } from "vue-i18n" ;
import { useRouter } from "vue-router" ;
import { ref , computed , inject } from "vue" ;
import { formatDateTimeString } from "@/filters/datetime" ;
import AccountCircle from "vue-material-design-icons/AccountCircle.vue" ;
import { Dialog } from "@/plugins/dialog" ;
import { Notifier } from "@/plugins/notifier" ;
import EventCard from "@/components/Event/EventCard.vue" ;
const router = useRouter ( ) ;
const props = defineProps < { reportId : string } > ( ) ;
const { currentActor } = useCurrentActorClient ( ) ;
const { result : reportResult , onError : onReportQueryError } = useQuery < {
report : IReport ;
} > ( REPORT , ( ) => ( {
id : props . reportId ,
} ) ) ;
const report = computed ( ( ) => reportResult . value ? . report ) ;
onReportQueryError ( ( { graphQLErrors } ) => {
errors . value = uniq (
graphQLErrors . map ( ( { message } : GraphQLError ) => message )
) ;
} ) ;
const { t } = useI18n ( { useScope : "global" } ) ;
useHead ( {
title : computed ( ( ) => t ( "Report" ) ) ,
} ) ;
const notifier = inject < Notifier > ( "notifier" ) ;
const errors = ref < string [ ] > ( [ ] ) ;
const noteContent = ref ( "" ) ;
const {
mutate : createReportNoteMutation ,
onDone : createReportNoteMutationDone ,
onError : createReportNoteMutationError ,
} = useMutation < {
createReportNote : IReportNote ;
} > ( CREATE _REPORT _NOTE , ( ) => ( {
update : (
store : ApolloCache < { createReportNote : IReportNote } > ,
{ data } : FetchResult
) => {
if ( data == null ) return ;
const cachedData = store . readQuery < { report : IReport } > ( {
query : REPORT ,
variables : { id : report . value ? . id } ,
} ) ;
if ( cachedData == null ) return ;
const { report : cachedReport } = cachedData ;
if ( cachedReport === null ) {
console . error ( "Cannot update event notes cache, because of null value." ) ;
return ;
}
const note = data . createReportNote ;
note . moderator = currentActor . value ;
cachedReport . notes = cachedReport . notes . concat ( [ note ] ) ;
store . writeQuery ( {
query : REPORT ,
variables : { id : report . value ? . id } ,
data : { report } ,
} ) ;
} ,
} ) ) ;
createReportNoteMutationDone ( ( ) => {
noteContent . value = "" ;
} ) ;
createReportNoteMutationError ( ( error ) => {
console . error ( error ) ;
} ) ;
const dialog = inject < Dialog > ( "dialog" ) ;
const confirmEventDelete = ( ) : void => {
dialog ? . confirm ( {
title : t ( "Deleting event" ) ,
message : t (
"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."
) ,
confirmText : t ( "Delete Event" ) ,
variant : "danger" ,
hasIcon : true ,
onConfirm : ( ) => deleteEvent ( ) ,
} ) ;
} ;
const confirmCommentDelete = ( comment : IComment ) : void => {
dialog ? . confirm ( {
title : t ( "Deleting comment" ) ,
message : t (
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone."
) ,
confirmText : t ( "Delete Comment" ) ,
variant : "danger" ,
hasIcon : true ,
onConfirm : ( ) => deleteCommentMutation ( { commentId : comment . id } ) ,
} ) ;
} ;
const {
mutate : deleteEventMutation ,
onDone : deleteEventMutationDone ,
onError : deleteEventMutationError ,
} = useMutation < { deleteEvent : { id : string } } > ( DELETE _EVENT ) ;
deleteEventMutationDone ( ( ) => {
const eventTitle = report . value ? . event ? . title ;
notifier ? . success (
t ( "Event {eventTitle} deleted" , {
eventTitle ,
} )
) ;
} ) ;
deleteEventMutationError ( ( error ) => {
console . error ( error ) ;
} ) ;
const deleteEvent = async ( ) : Promise < void > => {
if ( ! report . value ? . event ? . id ) return ;
deleteEventMutation ( { eventId : report . value . event . id } ) ;
} ;
const {
mutate : deleteCommentMutation ,
onDone : deleteCommentMutationDone ,
onError : deleteCommentMutationError ,
} = useMutation < { deleteComment : { id : string } } > ( DELETE _COMMENT ) ;
deleteCommentMutationDone ( ( ) => {
notifier ? . success ( t ( "Comment deleted" ) as string ) ;
} ) ;
deleteCommentMutationError ( ( error ) => {
console . error ( error ) ;
} ) ;
const {
mutate : updateReportMutation ,
onDone : onUpdateReportMutation ,
onError : onUpdateReportError ,
} = useMutation ( UPDATE _REPORT , ( ) => ( {
update : (
store : ApolloCache < { updateReportStatus : IReport } > ,
{ 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 event notes cache, because of null value." ) ;
return ;
}
const updatedReport = {
... cachedReport ,
status : data . updateReportStatus . status ,
} ;
store . writeQuery ( {
query : REPORT ,
variables : { id : report . value ? . id } ,
data : { report : updatedReport } ,
} ) ;
} ,
} ) ) ;
onUpdateReportMutation ( ( ) => {
router . push ( { name : RouteName . REPORTS } ) ;
} ) ;
onUpdateReportError ( ( error ) => {
console . error ( error ) ;
} ) ;
const updateReport = async ( status : ReportStatusEnum ) : Promise < void > => {
updateReportMutation ( {
reportId : report . value ? . id ,
status ,
} ) ;
} ;
< / script >
< style lang = "scss" scoped >
tbody td img . image ,
. note img . image {
display : inline ;
height : 1.5 em ;
vertical - align : text - bottom ;
}
. dialog . modal - card - foot {
justify - content : flex - end ;
}
. box a {
text - decoration : none ;
color : inherit ;
}
< / style >