mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-05 03:57:03 -04:00
477 lines
20 KiB
Vue
477 lines
20 KiB
Vue
<template>
|
|
<div v-if="$user() && $user().username !== user.username">
|
|
<section>
|
|
<a v-if="!showReportForm" href="#" class="small" @click.prevent="showReportForm = true">
|
|
<Icon v="spider" />
|
|
<T>report.action</T>
|
|
</a>
|
|
<div v-else-if="!reported">
|
|
<textarea
|
|
v-model="reportComment"
|
|
class="form-control"
|
|
rows="3"
|
|
:placeholder="$t('report.comment')"
|
|
:disabled="saving"
|
|
required
|
|
></textarea>
|
|
<div class="alert alert-info small mt-3">
|
|
<p><T>report.terms</T><T>quotation.colon</T></p>
|
|
<blockquote>
|
|
<T>terms.content.content.violations</T>
|
|
<template v-for="(violation, i) in forbidden">
|
|
<T>terms.content.content.violationsExamples.{{ violation }}</T><template v-if="i !== forbidden.length - 1">
|
|
,
|
|
</template>
|
|
</template>.
|
|
</blockquote>
|
|
<p class="mb-0">
|
|
<T>report.hoarding</T>
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="btn btn-danger d-block-force w-100 mt-2"
|
|
:disabled="saving || !reportComment"
|
|
@click="report"
|
|
>
|
|
<Icon v="spider" />
|
|
<T>report.action</T>
|
|
</button>
|
|
</div>
|
|
<div v-else class="alert alert-success">
|
|
<T>report.sent</T>
|
|
</div>
|
|
</section>
|
|
<section>
|
|
<a href="#" class="small" @click.prevent="block">
|
|
<Icon v="ban" />
|
|
<T>profile.blocks.action</T>
|
|
</a>
|
|
</section>
|
|
<section v-if="$isGranted('users') || $isGranted('community')">
|
|
<div v-if="banSnapshot" class="my-3">
|
|
<a
|
|
href="#"
|
|
class="badge bg-info"
|
|
@click.prevent="dialogue.alertRaw(banSnapshot)"
|
|
>
|
|
<Icon v="camera-polaroid" />
|
|
Show snapshot at the time of banning
|
|
</a>
|
|
</div>
|
|
<a v-if="!showBanForm" href="#" class="small" @click.prevent="showBanForm = true">
|
|
<Icon v="ban" />
|
|
<T>ban.action</T>
|
|
</a>
|
|
<div v-else>
|
|
<div class="alert alert-warning">
|
|
<h5>Ban proposals</h5>
|
|
<p>
|
|
After at least 2 moderators had proposed bans,
|
|
you'll be able to pick one of the proposals in order to actually issue the ban.
|
|
If the proposed reasons/term points significantly differ
|
|
or if you think the person shouldn't be banned despite another moderator thinking otherwise,
|
|
please start a thread in #moderation on Discord to discuss it.
|
|
</p>
|
|
<div class="table-responsive">
|
|
<table class="table table-striped" :style="!user.bannedReason && user.bannedBy ? `opacity: 0.5` : ``">
|
|
<thead>
|
|
<tr>
|
|
<th>Proposed at</th>
|
|
<th>Proposed by</th>
|
|
<th>Reason</th>
|
|
<th>Terms</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="proposal in banProposals">
|
|
<td>{{ $datetime($ulidTime(proposal.id)) }}</td>
|
|
<td>
|
|
<a :href="`${getUrlForLocale('_')}/@${proposal.bannedByUsername}`" target="_blank" rel="noopener">
|
|
@{{ proposal.bannedByUsername }}
|
|
</a>
|
|
</td>
|
|
<td>{{ proposal.bannedReason }}</td>
|
|
<td>
|
|
<ul>
|
|
<li v-for="term in proposal.bannedTerms.split(',')">
|
|
{{ term }}
|
|
</li>
|
|
</ul>
|
|
</td>
|
|
<td>
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline-primary btn-sm"
|
|
@click="copyProposal(proposal)"
|
|
>
|
|
Copy
|
|
</button>
|
|
<button
|
|
v-if="canApplyBan"
|
|
type="button"
|
|
class="btn btn-outline-danger btn-sm"
|
|
@click="applyBan(proposal.id)"
|
|
>
|
|
Apply ban
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p v-if="!user.bannedReason && user.bannedBy">
|
|
<Icon v="check-circle" /> Ban (proposals) were cancelled / account was unbanned.
|
|
</p>
|
|
<p v-else>
|
|
<button
|
|
v-if="isBanned || banProposals.length > 0"
|
|
type="button"
|
|
class="btn btn-success btn-sm"
|
|
@click="applyBan(0)"
|
|
>
|
|
Unban / cancel proposals
|
|
</button>
|
|
</p>
|
|
</div>
|
|
<textarea v-model="user.bannedReason" class="form-control" rows="3" :placeholder="`${$t('ban.reason')} ${$t('ban.visible')}`" :disabled="saving"></textarea>
|
|
<div class="form-group">
|
|
<p class="my-1">
|
|
<label><strong><T>ban.terms</T><T>quotation.colon</T></strong></label>
|
|
</p>
|
|
<div style="columns: 3" class="small">
|
|
<div v-for="violation in [...forbidden, 'miscellaneous']" class="form-check ps-0">
|
|
<label>
|
|
<input v-model="user.bannedTerms" type="checkbox" :value="violation" :disabled="violation === 'ableism' && new Date < new Date(2023, 3, 6)">
|
|
<T>terms.content.content.violationsExamples.{{ violation }}</T>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="btn btn-danger d-block-force w-100 mt-2"
|
|
:disabled="user.bannedReason === '' || saving"
|
|
@click="ban"
|
|
>
|
|
<Icon v="ban" />
|
|
<T>ban.action</T>
|
|
</button>
|
|
</div>
|
|
<AbuseReports v-if="abuseReports.length" :abuse-reports="abuseReports" allow-resolving />
|
|
</section>
|
|
<section v-if="$isGranted('users') || $isGranted('community')">
|
|
<a v-if="!showMessages" href="#" class="small" @click.prevent="showMessages = true">
|
|
<Icon v="comment-exclamation" />
|
|
Mod messages
|
|
</a>
|
|
<div v-else>
|
|
<h5>
|
|
<Icon v="comment-exclamation" />
|
|
Mod messages
|
|
</h5>
|
|
<p>
|
|
You can use this feature to warn a user without banning them, etc.
|
|
</p>
|
|
<table class="table table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Sent on</th>
|
|
<th>Sent by</th>
|
|
<th>Message</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="message in messages">
|
|
<td>{{ $datetime($ulidTime(message.id)) }}</td>
|
|
<td>
|
|
<a :href="`${getUrlForLocale('_')}/@${message.adminUsername}`" target="_blank" rel="noopener">
|
|
@{{ message.adminUsername }}
|
|
</a>
|
|
</td>
|
|
<td v-html="nl2br(message.message)"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<textarea v-model="message" class="form-control" rows="5" :disabled="saving" required></textarea>
|
|
<button
|
|
type="button"
|
|
class="btn btn-danger d-block-force w-100 mt-2"
|
|
:disabled="saving"
|
|
@click="sendMessage"
|
|
>
|
|
<Icon v="comment-exclamation" />
|
|
Send
|
|
</button>
|
|
<ul class="list-inline small mt-2">
|
|
<li class="list-inline-item">
|
|
Templates:
|
|
</li>
|
|
<li v-for="(templateContent, templateName) in modMessageTemplates" class="list-inline-item">
|
|
<a href="#" @click.prevent="message = templateContent">{{ templateName }}</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
<section v-if="($isGranted('users') || $isGranted('community')) && profile">
|
|
<a v-if="!showSensitive" href="#" class="small" @click.prevent="showSensitive = true">
|
|
<Icon v="engine-warning" />
|
|
Content warning
|
|
</a>
|
|
<div v-else>
|
|
<h5>
|
|
<Icon v="engine-warning" />
|
|
Content warning
|
|
</h5>
|
|
<ListInput v-model="sensitive" :maxlength="64" :maxitems="16" />
|
|
<button
|
|
type="button"
|
|
class="btn btn-danger d-block-force w-100 mt-2"
|
|
:disabled="saving"
|
|
@click="saveSensitive"
|
|
>
|
|
<Icon v="engine-warning" />
|
|
Overwrite CWs and notify user
|
|
</button>
|
|
</div>
|
|
</section>
|
|
<ModerationRules v-if="$isGranted('users') || $isGranted('community')" type="rulesUsers" class="mt-4" />
|
|
<div v-if="moderationQueueCaret === user.username" class="btn-group w-100">
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline-primary"
|
|
:disabled="moderationQueueIndex === 0"
|
|
@click="queuePrevious"
|
|
>
|
|
<Icon v="arrow-circle-left" />
|
|
Previous profile in the queue
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
:disabled="moderationQueueIndex === moderationQueue.length - 1"
|
|
@click="queueNext"
|
|
>
|
|
<Icon v="arrow-circle-right" />
|
|
Next profile in the queue
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import useDialogue from '../composables/useDialogue.ts';
|
|
import forbidden from '../src/forbidden.ts';
|
|
import { sleep } from '../src/helpers.ts';
|
|
|
|
import { getUrlForLocale } from '~/src/domain.ts';
|
|
|
|
export default {
|
|
props: {
|
|
user: { required: true },
|
|
profile: {},
|
|
},
|
|
setup() {
|
|
return {
|
|
dialogue: useDialogue(),
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
showReportForm: false,
|
|
reportComment: '',
|
|
reported: false,
|
|
|
|
showBanForm: !!this.user.bannedReason,
|
|
isBanned: !!this.user.bannedReason,
|
|
|
|
banSnapshot: undefined,
|
|
|
|
showMessages: false,
|
|
messages: undefined,
|
|
message: '',
|
|
|
|
showSensitive: (this.profile?.sensitive || []).length,
|
|
sensitive: this.profile?.sensitive,
|
|
|
|
saving: false,
|
|
|
|
forbidden,
|
|
|
|
abuseReports: [],
|
|
banProposals: [],
|
|
|
|
modMessageTemplates: {
|
|
'Generic content warning': `Hi!
|
|
|
|
The following content is in breach of our Terms of Service (https://en.pronouns.page/terms):
|
|
|
|
<insert context & more info here>
|
|
|
|
To keep our platform safe and inclusive, please remove this content.
|
|
|
|
Thanks!`,
|
|
'Suicide encouragement warning': `Hi!
|
|
|
|
<describe the suicide encouragement content here>
|
|
Suicide encouragement is prohibited by our Terms of Service (https://en.pronouns.page/terms).
|
|
|
|
To keep our platform safe and inclusive, please remove this content.
|
|
|
|
Thanks!
|
|
`,
|
|
'Underage account removal notice': `Hi!
|
|
|
|
We cannot process personal data of minors under 13 years old, as described in our Terms of Service: https://en.pronouns.page/terms
|
|
|
|
Unfortunately, I need to remove your account.
|
|
`,
|
|
},
|
|
|
|
moderationQueue: undefined,
|
|
moderationQueueCaret: undefined,
|
|
moderationQueueIndex: undefined,
|
|
};
|
|
},
|
|
computed: {
|
|
canApplyBan() {
|
|
return !this.isBanned && (this.$isGranted('users') || this.$isGranted('community')) && (this.banProposals.length >= 2 || this.$isGranted('*'));
|
|
},
|
|
},
|
|
async mounted() {
|
|
if (!this.$isGranted('users') && !this.$isGranted('community')) {
|
|
return;
|
|
}
|
|
if (this.$isSafari()) {
|
|
await sleep(500); // spread out less important requests, it seems to cause issues on safari
|
|
}
|
|
this.abuseReports = await $fetch(`/api/admin/reports/${this.user.id}`);
|
|
if (this.$isSafari()) {
|
|
await sleep(500);
|
|
}
|
|
this.banProposals = await $fetch(`/api/admin/ban-proposals/${encodeURIComponent(this.user.username)}`);
|
|
if (this.$isSafari()) {
|
|
await sleep(500);
|
|
}
|
|
this.banSnapshot = await $fetch(`/api/admin/ban-snapshot/${this.user.id}`);
|
|
if (this.banProposals.length > 0) {
|
|
this.showBanForm = true;
|
|
}
|
|
if (this.$isSafari()) {
|
|
await sleep(500);
|
|
}
|
|
this.messages = await $fetch(`/api/admin/mod-messages/${encodeURIComponent(this.user.username)}`);
|
|
if (this.messages.length > 0) {
|
|
this.showMessages = true;
|
|
}
|
|
|
|
this.moderationQueue = localStorage.getItem('moderationQueue');
|
|
this.moderationQueueCaret = localStorage.getItem('moderationQueueCaret');
|
|
|
|
if (this.moderationQueue) {
|
|
this.moderationQueue = this.moderationQueue.split(',');
|
|
this.moderationQueueIndex = this.moderationQueue.indexOf(this.moderationQueueCaret);
|
|
if (this.moderationQueueIndex < 0) {
|
|
this.moderationQueue = undefined;
|
|
this.moderationQueueCaret = undefined;
|
|
this.moderationQueueIndex = undefined;
|
|
localStorage.removeItem('moderationQueue');
|
|
localStorage.removeItem('moderationQueueCaret');
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
getUrlForLocale,
|
|
async ban() {
|
|
await this.dialogue.confirm(this.$t('ban.confirm', { username: this.user.username }), 'danger');
|
|
this.saving = true;
|
|
try {
|
|
await this.dialogue.postWithAlertOnError(`/api/admin/propose-ban/${encodeURIComponent(this.user.username)}`, {
|
|
reason: this.user.bannedReason,
|
|
terms: this.user.bannedTerms,
|
|
});
|
|
window.location.reload();
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
async applyBan(proposalId) {
|
|
await this.dialogue.confirm(this.$t(proposalId ? 'ban.confirm' : 'ban.confirmUnban', { username: this.user.username }), 'danger');
|
|
this.saving = true;
|
|
try {
|
|
await this.dialogue.postWithAlertOnError(`/api/admin/apply-ban/${encodeURIComponent(this.user.username)}/${proposalId}`);
|
|
window.location.reload();
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
async report() {
|
|
if (!this.reportComment) {
|
|
return;
|
|
}
|
|
await this.dialogue.confirm(this.$t('report.confirm', { username: this.user.username }), 'danger');
|
|
this.saving = true;
|
|
try {
|
|
await this.dialogue.postWithAlertOnError(`/api/profile/report/${encodeURIComponent(this.user.username)}`, {
|
|
comment: this.reportComment,
|
|
});
|
|
this.reported = true;
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
async block() {
|
|
await this.dialogue.confirm(this.$t('profile.blocks.confirm', { username: this.user.username }), 'danger');
|
|
await this.dialogue.postWithAlertOnError(`/api/user/block/${this.user.id}`);
|
|
window.location.reload();
|
|
},
|
|
copyProposal(proposal) {
|
|
this.user.bannedReason = proposal.bannedReason;
|
|
this.user.bannedTerms = proposal.bannedTerms.split(',');
|
|
},
|
|
async sendMessage() {
|
|
if (!this.message) {
|
|
return;
|
|
}
|
|
await this.dialogue.confirm(`<strong>Please proof-read and confirm sending:</strong><br/><br/><div class="text-start">${this.nl2br(this.message)}</div>`, 'danger');
|
|
this.saving = true;
|
|
try {
|
|
this.messages = await this.dialogue.postWithAlertOnError(`/api/admin/mod-message/${encodeURIComponent(this.user.username)}`, {
|
|
message: this.message,
|
|
});
|
|
this.message = '';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
async saveSensitive() {
|
|
await this.dialogue.confirm('Are you sure?', 'danger');
|
|
this.saving = true;
|
|
try {
|
|
await this.dialogue.postWithAlertOnError(`/api/admin/overwrite-sensitive/${encodeURIComponent(this.user.username)}`, {
|
|
sensitive: this.sensitive,
|
|
});
|
|
window.location.reload();
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
nl2br(text) {
|
|
return text.replace(new RegExp('\\n', 'g'), '<br/>');
|
|
},
|
|
queuePrevious() {
|
|
this.moderationQueueCaret = this.moderationQueue[this.moderationQueueIndex - 1];
|
|
localStorage.setItem('moderationQueueCaret', this.moderationQueueCaret);
|
|
this.$router.push(`/@${this.moderationQueueCaret}`);
|
|
},
|
|
queueNext() {
|
|
this.moderationQueueCaret = this.moderationQueue[this.moderationQueueIndex + 1];
|
|
localStorage.setItem('moderationQueueCaret', this.moderationQueueCaret);
|
|
this.$router.push(`/@${this.moderationQueueCaret}`);
|
|
},
|
|
},
|
|
};
|
|
</script>
|