2023-10-18 17:22:10 +00:00

366 lines
16 KiB
Vue

<template>
<div v-if="$user() && $user().username !== user.username">
<section v-if="$user()">
<a v-if="!showReportForm" href="#" @click.prevent="showReportForm = true" class="small">
<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 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 v-if="$isGranted('users')">
<div v-if="banSnapshot" class="my-3">
<a href="#" class="badge bg-info"
@click.prevent="$alertRaw(banSnapshot)"
>
<Icon v="camera-polaroid"/>
Show snapshot at the time of banning
</a>
</div>
<a v-if="!showBanForm" href="#" @click.prevent="showBanForm = true" class="small">
<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="`https://pronouns.page/@${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 class="btn btn-outline-primary btn-sm" @click="copyProposal(proposal)">Copy</button>
<button v-if="canApplyBan" 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" 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 class="form-check ps-0" v-for="violation in [...forbidden, 'miscellaneous']">
<label>
<input type="checkbox" :value="violation" v-model="user.bannedTerms" :disabled="violation === 'ableism' && new Date < new Date(2023, 3, 6)"/>
<T>terms.content.content.violationsExamples.{{violation}}</T>
</label>
</div>
</div>
</div>
<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" :abuseReports="abuseReports" allowResolving/>
</section>
<section v-if="$isGranted('users')">
<a v-if="!showMessages" href="#" @click.prevent="showMessages = true" class="small">
<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="`https://pronouns.page/@${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 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') && profile">
<a v-if="!showSensitive" href="#" @click.prevent="showSensitive = true" class="small">
<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 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')" type="rulesUsers" class="mt-4"/>
<div v-if="moderationQueueCaret === user.username" class="btn-group w-100">
<button class="btn btn-outline-primary"
:disabled="moderationQueueIndex === 0"
@click="queuePrevious"
>
<Icon v="arrow-circle-left"/>
Previous profile in the queue
</button>
<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 ClientOnly from 'vue-client-only'
import forbidden from "../src/forbidden";
export default {
components: { ClientOnly },
props: {
user: { required: true },
profile: {},
},
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!
`
},
moderationQueue: undefined,
moderationQueueCaret: undefined,
moderationQueueIndex: undefined,
}
},
async mounted() {
if (!this.$isGranted('users')) { return; }
this.abuseReports = await this.$axios.$get(`/admin/reports/${this.user.id}`);
this.banProposals = await this.$axios.$get(`/admin/ban-proposals/${encodeURIComponent(this.user.username)}`);
this.banSnapshot = await this.$axios.$get(`/admin/ban-snapshot/${this.user.id}`);
if (this.banProposals.length > 0) {
this.showBanForm = true;
}
this.messages = await this.$axios.$get(`/admin/mod-messages/${encodeURIComponent(this.user.username)}`);
if (this.messages.length > 0) {
this.showMessages = true;
}
if (!process.client) { return; }
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: {
async ban() {
await this.$confirm(this.$t('ban.confirm', {username: this.user.username}), 'danger');
this.saving = true;
try {
await this.$post(`/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.$confirm(this.$t(proposalId ? 'ban.confirm' : 'ban.confirmUnban', {username: this.user.username}), 'danger');
this.saving = true;
try {
await this.$post(`/admin/apply-ban/${encodeURIComponent(this.user.username)}/${proposalId}`);
window.location.reload();
} finally {
this.saving = false;
}
},
async report() {
if (!this.reportComment) { return; }
await this.$confirm(this.$t('report.confirm', {username: this.user.username}), 'danger');
this.saving = true;
try {
await this.$post(`/profile/report/${encodeURIComponent(this.user.username)}`, {
comment: this.reportComment,
});
this.reported = true;
} finally {
this.saving = false;
}
},
copyProposal(proposal) {
this.user.bannedReason = proposal.bannedReason;
this.user.bannedTerms = proposal.bannedTerms.split(',');
},
async sendMessage() {
if (!this.message) { return; }
await this.$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.$post(`/admin/mod-message/${encodeURIComponent(this.user.username)}`, {
message: this.message,
});
this.message = '';
} finally {
this.saving = false;
}
},
async saveSensitive() {
await this.$confirm(`Are you sure?`, 'danger');
this.saving = true;
try {
await this.$post(`/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);
},
},
computed: {
canApplyBan() {
return !this.isBanned && this.$isGranted('users') && (this.banProposals.length >= 2 || this.$isGranted('*'));
},
}
}
</script>