Allow to report a group

And multiple group tweaks

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-09-30 15:25:30 +02:00
parent cf070d7e67
commit 92367a5f33
33 changed files with 1088 additions and 754 deletions

View File

@@ -28,7 +28,7 @@
{{ $t("Reported by {reporter}", { reporter: report.reporter.preferredUsername }) }}
</span>
</div>
<div class="column" v-if="report.content">{{ report.content }}</div>
<div class="column" v-if="report.content" v-html="report.content" />
</div>
</div>
</div>

View File

@@ -100,15 +100,15 @@ export default class ReportModal extends Vue {
forward = false;
get translatedCancelText() {
return this.cancelText || this.$t("Cancel");
get translatedCancelText(): string {
return this.cancelText || (this.$t("Cancel") as string);
}
get translatedConfirmText() {
return this.confirmText || this.$t("Send the report");
get translatedConfirmText(): string {
return this.confirmText || (this.$t("Send the report") as string);
}
confirm() {
confirm(): void {
this.onConfirm(this.content, this.forward);
this.close();
}
@@ -116,7 +116,7 @@ export default class ReportModal extends Vue {
/**
* Close the Dialog.
*/
close() {
close(): void {
this.isActive = false;
this.$emit("close");
}

View File

@@ -114,7 +114,7 @@ export const REPORT = gql`
export const CREATE_REPORT = gql`
mutation CreateReport(
$eventId: ID!
$eventId: ID
$reporterId: ID!
$reportedId: ID!
$content: String

View File

@@ -773,5 +773,11 @@
"Will allow to display and manage your participation status on the event page when using this device. Uncheck if you're using a public device.": "Will allow to display and manage your participation status on the event page when using this device. Uncheck if you're using a public device.",
"Visit event page": "Visit event page",
"Remember my participation in this browser": "Remember my participation in this browser",
"Organized by": "Organized by"
"Organized by": "Organized by",
"Report this group": "Report this group",
"Group {groupTitle} reported": "Group {groupTitle} reported",
"Error while reporting group {groupTitle}": "Error while reporting group {groupTitle}",
"Reported group": "Reported group",
"You can only get invited to groups right now.": "You can only get invited to groups right now.",
"Join group": "Join group"
}

View File

@@ -810,5 +810,11 @@
"Will allow to display and manage your participation status on the event page when using this device. Uncheck if you're using a public device.": "Permet d'afficher et de gérer le statut de votre participation sur la page de l'événement lorsque vous utilisez cet appareil. Décochez si vous utilisez un appareil public.",
"Visit event page": "Voir la page de l'événement",
"Remember my participation in this browser": "Se souvenir de ma participation dans ce navigateur",
"Organized by": "Organisé par"
"Organized by": "Organisé par",
"Report this group": "Signaler ce groupe",
"Group {groupTitle} reported": "Groupe {groupTitle} signalé",
"Error while reporting group {groupTitle}": "Erreur lors du signalement du groupe {groupTitle}",
"Reported group": "Groupe signalé",
"You can only get invited to groups right now.": "Vous pouvez uniquement être invité aux groupes pour le moment.",
"Join group": "Rejoindre le groupe"
}

View File

@@ -413,7 +413,7 @@
<report-modal
:on-confirm="reportEvent"
:title="$t('Report this event')"
:outside-domain="event.organizerActor.domain"
:outside-domain="domainForReport"
@close="$refs.reportModal.close()"
/>
</b-modal>
@@ -521,7 +521,7 @@ import {
ParticipantRole,
EventJoinOptions,
} from "../../types/event.model";
import { IPerson, Person, usernameWithDomain } from "../../types/actor";
import { IActor, IPerson, Person, usernameWithDomain } from "../../types/actor";
import { GRAPHQL_API_ENDPOINT } from "../../api/_entrypoint";
import DateCalendarIcon from "../../components/Event/DateCalendarIcon.vue";
import EventCard from "../../components/Event/EventCard.vue";
@@ -786,7 +786,7 @@ export default class Event extends EventMixin {
variables: {
eventId: this.event.id,
reporterId,
reportedId: this.event.organizerActor.id,
reportedId: this.actorForReport ? this.actorForReport.id : null,
content,
forward,
},
@@ -1026,6 +1026,23 @@ export default class Event extends EventMixin {
this.config && (this.currentActor.id !== undefined || this.config.anonymous.reports.allowed)
);
}
get actorForReport(): IActor | null {
if (this.event.attributedTo && this.event.attributedTo.id) {
return this.event.attributedTo;
}
if (this.event.organizerActor) {
return this.event.organizerActor;
}
return null;
}
get domainForReport(): string | null {
if (this.actorForReport) {
return this.actorForReport.domain;
}
return null;
}
}
</script>
<style lang="scss" scoped>

View File

@@ -93,8 +93,8 @@
>
</p>
</div>
<div class="block-column address" v-else-if="physicalAddress">
<address>
<div class="block-column address" v-else>
<address v-if="physicalAddress">
<p class="addressDescription" :title="physicalAddress.poiInfos.name">
{{ physicalAddress.poiInfos.name }}
</p>
@@ -106,6 +106,27 @@
v-if="physicalAddress && physicalAddress.geom"
>{{ $t("Show map") }}</span
>
<p class="buttons">
<b-tooltip
:label="$t('You can only get invited to groups right now.')"
position="is-bottom"
>
<b-button disabled type="is-primary">{{ $t("Join group") }}</b-button>
</b-tooltip>
<b-dropdown aria-role="list" position="is-bottom-left">
<b-button slot="trigger" role="button" icon-right="dots-horizontal"> </b-button>
<b-dropdown-item
aria-role="listitem"
v-if="ableToReport"
@click="isReportModalActive = true"
>
<span>
{{ $t("Report") }}
<b-icon icon="flag" />
</span>
</b-dropdown-item>
</b-dropdown>
</p>
</div>
<img v-if="group.banner && group.banner.url" :src="group.banner.url" alt="" />
</header>
@@ -291,6 +312,14 @@
/>
</div>
</b-modal>
<b-modal :active.sync="isReportModalActive" has-modal-card ref="reportModal">
<report-modal
:on-confirm="reportGroup"
:title="$t('Report this group')"
:outside-domain="group.domain"
@close="$refs.reportModal.close()"
/>
</b-modal>
</div>
</div>
</template>
@@ -319,8 +348,13 @@ import FolderItem from "@/components/Resource/FolderItem.vue";
import { Address } from "@/types/address.model";
import Invitations from "@/components/Group/Invitations.vue";
import addMinutes from "date-fns/addMinutes";
import GroupSection from "../../components/Group/GroupSection.vue";
import { CONFIG } from "@/graphql/config";
import { CREATE_REPORT } from "@/graphql/report";
import { IReport } from "@/types/report.model";
import { IConfig } from "@/types/config.model";
import RouteName from "../../router/name";
import GroupSection from "../../components/Group/GroupSection.vue";
import ReportModal from "../../components/Report/ReportModal.vue";
@Component({
apollo: {
@@ -346,6 +380,7 @@ import RouteName from "../../router/name";
},
},
currentActor: CURRENT_ACTOR_CLIENT,
config: CONFIG,
},
components: {
DiscussionListItem,
@@ -358,6 +393,7 @@ import RouteName from "../../router/name";
ResourceItem,
GroupSection,
Invitations,
ReportModal,
"map-leaflet": () => import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
},
metaInfo() {
@@ -385,6 +421,8 @@ export default class Group extends Vue {
group: IGroup = new GroupModel();
config!: IConfig;
loading = true;
RouteName = RouteName;
@@ -393,6 +431,8 @@ export default class Group extends Vue {
showMap = false;
isReportModalActive = false;
@Watch("currentActor")
watchCurrentActor(currentActor: IActor, oldActor: IActor): void {
if (currentActor.id && oldActor && currentActor.id !== oldActor.id) {
@@ -414,6 +454,38 @@ export default class Group extends Vue {
}
}
async reportGroup(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 groupTitle = this.group.name || usernameWithDomain(this.group);
let reporterId = null;
if (this.currentActor.id) {
reporterId = this.currentActor.id;
} else if (this.config.anonymous.reports.allowed) {
reporterId = this.config.anonymous.actorId;
}
if (!reporterId) return;
try {
await this.$apollo.mutate<IReport>({
mutation: CREATE_REPORT,
variables: {
reporterId,
reportedId: this.group.id,
content,
forward,
},
});
this.$notifier.success(this.$t("Group {groupTitle} reported", { groupTitle }) as string);
} catch (error) {
console.error(error);
this.$notifier.error(
this.$t("Error while reporting group {groupTitle}", { groupTitle }) as string
);
}
}
get groupTitle(): undefined | string {
if (!this.group) return undefined;
return this.group.name || this.group.preferredUsername;
@@ -497,6 +569,12 @@ export default class Group extends Vue {
if (!this.group.physicalAddress) return null;
return new Address(this.group.physicalAddress);
}
get ableToReport(): boolean {
return (
this.config && (this.currentActor.id !== undefined || this.config.anonymous.reports.allowed)
);
}
}
</script>
<style lang="scss" scoped>
@@ -523,7 +601,6 @@ div.container {
border: 2px solid $purple-2;
padding: 10px 0;
position: relative;
overflow: hidden;
h1 {
color: $purple-1;
@@ -545,8 +622,10 @@ div.container {
left: 0;
top: 0;
width: 100%;
height: auto;
height: 100%;
opacity: 0.3;
object-fit: cover;
object-position: 50% 50%;
}
}
@@ -571,6 +650,16 @@ div.container {
cursor: pointer;
}
p.buttons {
margin-top: 1rem;
justify-content: end;
align-content: space-between;
& > span {
margin-right: 0.5rem;
}
}
address {
font-style: normal;

View File

@@ -43,8 +43,29 @@
<div class="table-container">
<table class="table is-striped is-fullwidth">
<tbody>
<tr>
<td>{{ $t("Reported identity") }}</td>
<tr v-if="report.reported.__typename === '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) }}
</router-link>
</td>
</tr>
<tr v-else>
<td>
{{ $t("Reported identity") }}
</td>
<td>
<router-link
:to="{
@@ -58,7 +79,7 @@
:src="report.reported.avatar.url"
alt=""
/>
@{{ report.reported.preferredUsername }}
{{ displayNameAndUsername(report.reported) }}
</router-link>
</td>
</tr>
@@ -80,7 +101,7 @@
:src="report.reporter.avatar.url"
alt=""
/>
@{{ report.reporter.preferredUsername }}
{{ displayNameAndUsername(report.reporter) }}
</router-link>
</td>
</tr>
@@ -157,38 +178,40 @@
>
</div>
<ul v-for="comment in report.comments" v-if="report.comments.length > 0" :key="comment.id">
<li>
<div class="box" v-if="comment">
<article class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="comment.actor && comment.actor.avatar">
<img :src="comment.actor.avatar.url" alt="Image" />
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
</div>
<div class="media-content">
<div class="content">
<span v-if="comment.actor">
<strong>{{ comment.actor.name }}</strong>
<small>@{{ comment.actor.preferredUsername }}</small>
</span>
<span v-else>{{ $t("Unknown actor") }}</span>
<br />
<p v-html="comment.text" />
<div v-if="report.comments.length > 0">
<ul v-for="comment in report.comments" :key="comment.id">
<li>
<div class="box" v-if="comment">
<article class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="comment.actor && comment.actor.avatar">
<img :src="comment.actor.avatar.url" alt="Image" />
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
</div>
<b-button
type="is-danger"
@click="confirmCommentDelete(comment)"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
>
</div>
</article>
</div>
</li>
</ul>
<div class="media-content">
<div class="content">
<span v-if="comment.actor">
<strong>{{ comment.actor.name }}</strong>
<small>@{{ comment.actor.preferredUsername }}</small>
</span>
<span v-else>{{ $t("Unknown actor") }}</span>
<br />
<p v-html="comment.text" />
</div>
<b-button
type="is-danger"
@click="confirmCommentDelete(comment)"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
>
</div>
</article>
</div>
</li>
</ul>
</div>
<h2 class="title" v-if="report.notes.length > 0">{{ $t("Notes") }}</h2>
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`" :key="note.id">
@@ -220,7 +243,7 @@ import { Component, Prop, Vue } from "vue-property-decorator";
import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from "@/graphql/report";
import { IReport, IReportNote, ReportStatusEnum } from "@/types/report.model";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { IPerson, ActorType } from "@/types/actor";
import { IPerson, ActorType, displayNameAndUsername } from "@/types/actor";
import { DELETE_EVENT } from "@/graphql/event";
import { uniq } from "lodash";
import { nl2br } from "@/utils/html";
@@ -272,7 +295,9 @@ export default class Report extends Vue {
noteContent = "";
addNote() {
displayNameAndUsername = displayNameAndUsername;
addNote(): void {
try {
this.$apollo.mutate<{ createReportNote: IReportNote }>({
mutation: CREATE_REPORT_NOTE,
@@ -312,7 +337,7 @@ export default class Report extends Vue {
}
}
confirmEventDelete() {
confirmEventDelete(): void {
this.$buefy.dialog.confirm({
title: this.$t("Deleting event") as string,
message: this.$t(
@@ -325,7 +350,7 @@ export default class Report extends Vue {
});
}
confirmCommentDelete(comment: IComment) {
confirmCommentDelete(comment: IComment): void {
this.$buefy.dialog.confirm({
title: this.$t("Deleting comment") as string,
message: this.$t(
@@ -338,7 +363,7 @@ export default class Report extends Vue {
});
}
async deleteEvent() {
async deleteEvent(): Promise<void> {
if (!this.report.event || !this.report.event.id) return;
const eventTitle = this.report.event.title;
@@ -364,7 +389,7 @@ export default class Report extends Vue {
}
}
async deleteComment(comment: IComment) {
async deleteComment(comment: IComment): Promise<void> {
try {
await this.$apollo.mutate({
mutation: DELETE_COMMENT,
@@ -379,7 +404,7 @@ export default class Report extends Vue {
}
}
async updateReport(status: ReportStatusEnum) {
async updateReport(status: ReportStatusEnum): Promise<void> {
try {
await this.$apollo.mutate({
mutation: UPDATE_REPORT,
@@ -415,27 +440,6 @@ export default class Report extends Vue {
console.error(error);
}
}
// TODO make me a global function
formatDate(value: string) {
return value
? new Date(value).toLocaleString(undefined, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
: null;
}
formatTime(value: string) {
return value
? new Date(value).toLocaleTimeString(undefined, {
hour: "numeric",
minute: "numeric",
})
: null;
}
}
</script>
<style lang="scss" scoped>

View File

@@ -44,7 +44,7 @@
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { Component, Vue, Watch } from "vue-property-decorator";
import { IReport, ReportStatusEnum } from "@/types/report.model";
import { REPORTS } from "@/graphql/report";
import ReportCard from "@/components/Report/ReportCard.vue";
@@ -77,7 +77,7 @@ export default class ReportList extends Vue {
filterReports: ReportStatusEnum = ReportStatusEnum.OPEN;
@Watch("$route.params.filter", { immediate: true })
onRouteFilterChanged(val: string) {
onRouteFilterChanged(val: string): void {
if (!val) return;
const filter = val.toUpperCase();
if (filter in ReportStatusEnum) {
@@ -86,7 +86,7 @@ export default class ReportList extends Vue {
}
@Watch("filterReports", { immediate: true })
async onFilterChanged(val: string) {
async onFilterChanged(val: string): Promise<void> {
await this.$router.push({
name: RouteName.REPORTS,
params: { filter: val.toLowerCase() },