123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155 |
- <template>
- <div>
- <div class="header bg-primary pb-3 mt-n4">
- <div class="container-fluid">
- <div class="header-body">
- <div class="row align-items-center py-4">
- <div class="col-lg-6 col-7">
- <p class="display-1 text-white d-inline-block mb-0">Moderation</p>
- </div>
- </div>
- <div class="row">
- <div class="col-12 col-sm-6 col-lg-3">
- <div class="mb-3">
- <h5 class="text-light text-uppercase mb-0">Active Reports</h5>
- <span
- class="text-white h2 font-weight-bold mb-0 human-size"
- data-toggle="tooltip"
- data-placement="bottom"
- :title="stats.open + ' open reports'">
- {{ prettyCount(stats.open) }}
- </span>
- </div>
- </div>
- <div class="col-12 col-sm-6 col-lg-3">
- <div class="mb-3">
- <h5 class="text-light text-uppercase mb-0">Active Spam Detections</h5>
- <span
- class="text-white h2 font-weight-bold mb-0 human-size"
- data-toggle="tooltip"
- data-placement="bottom"
- :title="stats.autospam_open + ' open spam detections'"
- >{{ prettyCount(stats.autospam_open) }}</span>
- </div>
- </div>
- <div class="col-12 col-sm-6 col-lg-3">
- <div class="mb-3">
- <h5 class="text-light text-uppercase mb-0">Total Reports</h5>
- <span
- class="text-white h2 font-weight-bold mb-0 human-size"
- data-toggle="tooltip"
- data-placement="bottom"
- :title="stats.total + ' total reports'"
- >{{ prettyCount(stats.total) }}
- </span>
- </div>
- </div>
- <div class="col-12 col-sm-6 col-lg-3">
- <div class="mb-3">
- <h5 class="text-light text-uppercase mb-0">Total Spam Detections</h5>
- <span
- class="text-white h2 font-weight-bold mb-0 human-size"
- data-toggle="tooltip"
- data-placement="bottom"
- :title="stats.autospam + ' total spam detections'">
- {{ prettyCount(stats.autospam) }}
- </span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div v-if="!loaded" class="my-5 text-center">
- <b-spinner />
- </div>
- <div v-else class="m-n2 m-lg-4">
- <div class="container-fluid mt-4">
- <div class="row mb-3 justify-content-between">
- <div class="col-12">
- <ul class="nav nav-pills">
- <li class="nav-item">
- <a
- :class="['nav-link d-flex align-items-center', { active: tabIndex == 0}]"
- href="#"
- @click.prevent="toggleTab(0)">
- <span>Open Reports</span>
- <span
- v-if="stats.open"
- class="badge badge-sm badge-floating badge-danger border-white ml-2"
- style="background-color: red;color:white;font-size:11px;">
- {{prettyCount(stats.open)}}
- </span>
- </a>
- </li>
- <li class="nav-item">
- <a
- :class="['nav-link d-flex align-items-center', { active: tabIndex == 2}]"
- href="#"
- @click.prevent="toggleTab(2)">
- <span>Spam Detections</span>
- <span
- v-if="stats.autospam_open"
- class="badge badge-sm badge-floating badge-danger border-white ml-2"
- style="background-color: red;color:white;font-size:11px;">
- {{prettyCount(stats.autospam_open)}}
- </span>
- </a>
- </li>
- <li class="nav-item">
- <a
- :class="['nav-link d-flex align-items-center', { active: tabIndex == 3}]"
- href="#"
- @click.prevent="toggleTab(3)">
- <span>Remote Reports</span>
- <span
- v-if="stats.remote_open"
- class="badge badge-sm badge-floating badge-danger border-white ml-2"
- style="background-color: red;color:white;font-size:11px;">
- {{prettyCount(stats.remote_open)}}
- </span>
- </a>
- </li>
- <li class="d-none d-md-block nav-item">
- <a
- :class="['nav-link d-flex align-items-center', { active: tabIndex == 1}]"
- href="#"
- @click.prevent="toggleTab(1)">
- <span>Closed Reports</span>
- <span
- v-if="stats.autospam_open"
- class="badge badge-sm badge-floating badge-secondary border-white ml-2"
- style="font-size:11px;">
- {{prettyCount(stats.closed)}}
- </span>
- </a>
- </li>
- <li class="d-none d-md-block nav-item">
- <a
- href="/i/admin/reports/email-verifications"
- class="nav-link d-flex align-items-center">
- <span>Email Verification Requests</span>
- <span
- v-if="stats.email_verification_requests"
- class="badge badge-sm badge-floating badge-secondary border-white ml-2"
- style="font-size:11px;">
- {{prettyCount(stats.email_verification_requests)}}
- </span>
- </a>
- </li>
- <li class="d-none d-md-block nav-item">
- <a
- href="/i/admin/reports/appeals"
- class="nav-link d-flex align-items-center">
- <span>Appeal Requests</span>
- <span
- v-if="stats.appeals"
- class="badge badge-sm badge-floating badge-secondary border-white ml-2"
- style="font-size:11px;">
- {{ prettyCount(stats.appeals) }}
- </span>
- </a>
- </li>
- </ul>
- </div>
- </div>
- <div v-if="[0, 1].includes(this.tabIndex)" class="table-responsive rounded">
- <table v-if="reports && reports.length" class="table table-dark">
- <thead class="thead-dark">
- <tr>
- <th scope="col">ID</th>
- <th scope="col">Report</th>
- <th scope="col">Reported Account</th>
- <th scope="col">Reported By</th>
- <th scope="col">Created</th>
- <th scope="col">View Report</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="(report, idx) in reports">
- <td class="font-weight-bold text-monospace text-muted align-middle">
- <a href="#" @click.prevent="viewReport(report)">
- {{ report.id }}
- </a>
- </td>
- <td class="align-middle">
- <p class="text-capitalize font-weight-bold mb-0" v-html="reportLabel(report)"></p>
- </td>
- <td class="align-middle">
- <a v-if="report.reported && report.reported.id" :href="`/i/web/profile/${report.reported.id}`" target="_blank" class="text-white">
- <div class="d-flex align-items-center" style="gap:0.61rem;">
- <img
- :src="report.reported.avatar"
- width="30"
- height="30"
- style="object-fit: cover;border-radius:30px;"
- onerror="this.src='/storage/avatars/default.png';this.error=null;">
- <div class="d-flex flex-column">
- <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reported.username}}</p>
- <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
- <span>{{report.reported.followers_count}} Followers</span>
- <span>·</span>
- <span>Joined {{ timeAgo(report.reported.created_at) }}</span>
- </div>
- </div>
- </div>
- </a>
- </td>
- <td class="align-middle">
- <a
- v-if="report && report.reporter && report.reporter.id"
- :href="`/i/web/profile/${report.reporter.id}`"
- target="_blank"
- class="text-white">
- <div class="d-flex align-items-center" style="gap:0.61rem;">
- <img
- :src="report.reporter.avatar"
- width="30"
- height="30"
- style="object-fit: cover;border-radius:30px;"
- onerror="this.src='/storage/avatars/default.png';this.error=null;">
- <div class="d-flex flex-column">
- <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reporter.username}}</p>
- <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
- <span>{{report.reporter.followers_count}} Followers</span>
- <span>·</span>
- <span>Joined {{ timeAgo(report.reporter.created_at) }}</span>
- </div>
- </div>
- </div>
- </a>
- </td>
- <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
- <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="viewReport(report)">View</a></td>
- </tr>
- </tbody>
- </table>
- <div v-else>
- <div class="card card-body p-5">
- <div class="d-flex justify-content-between align-items-center flex-column">
- <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
- <p class="lead">{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}</p>
- </div>
- </div>
- </div>
- </div>
- <div v-if="[0, 1].includes(this.tabIndex) && reports.length && (pagination.prev || pagination.next)" class="d-flex align-items-center justify-content-center">
- <button
- class="btn btn-primary rounded-pill"
- :disabled="!pagination.prev"
- @click="paginate('prev')">
- Prev
- </button>
- <button
- class="btn btn-primary rounded-pill"
- :disabled="!pagination.next"
- @click="paginate('next')">
- Next
- </button>
- </div>
- <div v-if="this.tabIndex === 2" class="table-responsive rounded">
- <template v-if="autospamLoaded">
- <table v-if="autospam && autospam.length" class="table table-dark">
- <thead class="thead-dark">
- <tr>
- <th scope="col">ID</th>
- <th scope="col">Report</th>
- <th scope="col">Reported Account</th>
- <th scope="col">Created</th>
- <th scope="col">View Report</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="(report, idx) in autospam">
- <td class="font-weight-bold text-monospace text-muted align-middle">
- <a href="#" @click.prevent="viewSpamReport(report)">
- {{ report.id }}
- </a>
- </td>
- <td class="align-middle">
- <p class="text-capitalize font-weight-bold mb-0">Spam Post</p>
- </td>
- <td class="align-middle">
- <a v-if="report.status && report.status.account" :href="`/i/web/profile/${report.status.account.id}`" target="_blank" class="text-white">
- <div class="d-flex align-items-center" style="gap:0.61rem;">
- <img
- :src="report.status.account.avatar"
- width="30"
- height="30"
- style="object-fit: cover;border-radius:30px;"
- onerror="this.src='/storage/avatars/default.png';this.error=null;">
- <div class="d-flex flex-column">
- <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.status.account.username}}</p>
- <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
- <span>{{report.status.account.followers_count}} Followers</span>
- <span>·</span>
- <span>Joined {{ timeAgo(report.status.account.created_at) }}</span>
- </div>
- </div>
- </div>
- </a>
- </td>
- <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
- <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="viewSpamReport(report)">View</a></td>
- </tr>
- </tbody>
- </table>
- <div v-else>
- <div class="card card-body p-5">
- <div class="d-flex justify-content-between align-items-center flex-column">
- <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
- <p class="lead">No Spam Reports Found!</p>
- </div>
- </div>
- </div>
- </template>
- <div v-else class="d-flex align-items-center justify-content-center" style="min-height: 300px;">
- <b-spinner />
- </div>
- </div>
- <div v-if="this.tabIndex === 2 && autospamLoaded && autospam && autospam.length" class="d-flex align-items-center justify-content-center">
- <button
- class="btn btn-primary rounded-pill"
- :disabled="!autospamPagination.prev"
- @click="autospamPaginate('prev')">
- Prev
- </button>
- <button
- class="btn btn-primary rounded-pill"
- :disabled="!autospamPagination.next"
- @click="autospamPaginate('next')">
- Next
- </button>
- </div>
- <div v-if="this.tabIndex === 3" class="table-responsive rounded">
- <table v-if="reports && reports.length" class="table table-dark">
- <thead class="thead-dark">
- <tr>
- <th scope="col">ID</th>
- <th scope="col">Instance</th>
- <th scope="col">Reported Account</th>
- <th scope="col">Comment</th>
- <th scope="col">Created</th>
- <th scope="col">View Report</th>
- </tr>
- </thead>
- <tbody>
- <tr
- v-for="(report, idx) in reports"
- :key="`remote-reports-${report.id}-${idx}`">
- <td class="font-weight-bold text-monospace text-muted align-middle">
- <a href="#" @click.prevent="showRemoteReport(report)">
- {{ report.id }}
- </a>
- </td>
- <td class="align-middle">
- <p class="font-weight-bold mb-0">{{ report.instance }}</p>
- </td>
- <td class="align-middle">
- <a v-if="report.reported && report.reported.id" :href="`/i/web/profile/${report.reported.id}`" target="_blank" class="text-white">
- <div class="d-flex align-items-center" style="gap:0.61rem;">
- <img
- :src="report.reported.avatar"
- width="30"
- height="30"
- style="object-fit: cover;border-radius:30px;"
- onerror="this.src='/storage/avatars/default.png';this.error=null;">
- <div class="d-flex flex-column">
- <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reported.username}}</p>
- <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
- <span>{{report.reported.followers_count}} Followers</span>
- <span>·</span>
- <span>Joined {{ timeAgo(report.reported.created_at) }}</span>
- </div>
- </div>
- </div>
- </a>
- </td>
- <td class="align-middle">
- <p class="small mb-0 text-wrap" style="max-width: 300px;word-break: break-all;">{{ report.message && report.message.length > 120 ? report.message.slice(0, 120) + '...' : report.message }}</p>
- </td>
- <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
- <td class="align-middle"><a href="#" class="btn btn-primary btn-sm">View</a></td>
- </tr>
- </tbody>
- </table>
- <div v-else>
- <div class="card card-body p-5">
- <div class="d-flex justify-content-between align-items-center flex-column">
- <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
- <p class="lead">{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}</p>
- </div>
- </div>
- </div>
- </div>
- <div v-if="this.tabIndex === 3 && remoteReportsLoaded && reports && reports.length" class="d-flex align-items-center justify-content-center">
- <button
- class="btn btn-primary rounded-pill"
- :disabled="!pagination.prev"
- @click="remoteReportPaginate('prev')">
- Prev
- </button>
- <button
- class="btn btn-primary rounded-pill"
- :disabled="!pagination.next"
- @click="remoteReportPaginate('next')">
- Next
- </button>
- </div>
- </div>
- </div>
- <b-modal v-model="showReportModal" :title="tabIndex === 0 ? 'View Report' : 'Viewing Closed Report'" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
- <div v-if="viewingReportLoading" class="d-flex align-items-center justify-content-center">
- <b-spinner />
- </div>
- <template v-else>
- <div v-if="viewingReport" class="list-group">
- <div class="list-group-item d-flex align-items-center justify-content-between">
- <div class="text-muted small">Type</div>
- <div class="font-weight-bold text-capitalize" v-html="reportLabel(viewingReport)"></div>
- </div>
- <div v-if="viewingReport.admin_seen_at" class="list-group-item d-flex align-items-center justify-content-between">
- <div class="text-muted small">Report Closed</div>
- <div class="font-weight-bold text-capitalize">{{ formatDate(viewingReport.admin_seen_at) }}</div>
- </div>
- <div v-if="viewingReport.reporter_message" class="list-group-item d-flex flex-column" style="gap:10px;">
- <div class="text-muted small">Message</div>
- <p class="mb-0 read-more" style="font-size: 12px;overflow-y: hidden;">{{ viewingReport.reporter_message }}</p>
- </div>
- </div>
- <div class="list-group list-group-horizontal mt-3">
- <div v-if="viewingReport && viewingReport.reported" class="list-group-item d-flex align-items-center justify-content-between flex-column flex-grow-1" style="gap:0.4rem;">
- <div class="text-muted small font-weight-bold mt-n1">Reported Account</div>
- <a v-if="viewingReport.reported && viewingReport.reported.id" :href="`/i/web/profile/${viewingReport.reported.id}`" target="_blank" class="text-primary">
- <div class="d-flex align-items-center" style="gap:0.61rem;">
- <img
- :src="viewingReport.reported.avatar"
- width="30"
- height="30"
- style="object-fit: cover;border-radius:30px;"
- onerror="this.src='/storage/avatars/default.png';this.error=null;">
- <div class="d-flex flex-column">
- <p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;" :class="[ viewingReport.reported.is_admin ? 'text-danger': '']">@{{viewingReport.reported.acct}}</p>
- <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
- <span>{{viewingReport.reported.followers_count}} Followers</span>
- <span>·</span>
- <span>Joined {{ timeAgo(viewingReport.reported.created_at) }}</span>
- </div>
- </div>
- </div>
- </a>
- </div>
- <div v-if="viewingReport && viewingReport.reporter" class="list-group-item d-flex align-items-center justify-content-between flex-column flex-grow-1" style="gap:0.4rem;">
- <div class="text-muted small font-weight-bold mt-n1">Reporter Account</div>
- <a v-if="viewingReport.reporter && viewingReport.reporter?.id" :href="`/i/web/profile/${viewingReport.reporter?.id}`" target="_blank" class="text-primary">
- <div class="d-flex align-items-center" style="gap:0.61rem;">
- <img
- :src="viewingReport.reporter.avatar"
- width="30"
- height="30"
- style="object-fit: cover;border-radius:30px;"
- onerror="this.src='/storage/avatars/default.png';this.error=null;">
- <div class="d-flex flex-column">
- <p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;">@{{viewingReport.reporter.acct}}</p>
- <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
- <span>{{viewingReport.reporter.followers_count}} Followers</span>
- <span>·</span>
- <span>Joined {{ timeAgo(viewingReport.reporter.created_at) }}</span>
- </div>
- </div>
- </div>
- </a>
- </div>
- </div>
- <div v-if="viewingReport && viewingReport.object_type === 'App\\Status' && viewingReport.status" class="list-group mt-3">
- <div v-if="viewingReport && viewingReport.status && viewingReport.status.media_attachments.length" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
- <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
- <div>Reported Post</div>
- <a class="font-weight-bold" :href="viewingReport.status.url" target="_blank">View</a>
- </div>
- <img
- v-if="viewingReport.status.media_attachments[0].type === 'image'"
- :src="viewingReport.status.media_attachments[0].url"
- height="140"
- class="rounded"
- style="object-fit: cover;"
- onerror="this.src='/storage/no-preview.png';this.error=null;" />
- <video
- v-else-if="viewingReport.status.media_attachments[0].type === 'video'"
- height="140"
- controls
- :src="viewingReport.status.media_attachments[0].url"
- onerror="this.src='/storage/no-preview.png';this.onerror=null;"
- ></video>
- </div>
- <div v-if="viewingReport && viewingReport.status" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
- <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
- <div>Reported Post Caption</div>
- <a class="font-weight-bold" :href="viewingReport.status.url" target="_blank">View</a>
- </div>
- <p class="mb-0 read-more" style="font-size:12px;overflow-y: hidden">{{ viewingReport.status.content_text }}</p>
- </div>
- </div>
- <div v-else-if="viewingReport && viewingReport.object_type === 'App\\Story' && viewingReport.story" class="list-group mt-3">
- <div v-if="viewingReport && viewingReport.story" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
- <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
- <div>Reported Story</div>
- <a class="font-weight-bold" :href="viewingReport.story.url" target="_blank">View</a>
- </div>
- <img
- v-if="viewingReport.story.type === 'photo'"
- :src="viewingReport.story.media_src"
- height="140"
- class="rounded"
- style="object-fit: cover;"
- onerror="this.src='/storage/no-preview.png';this.error=null;" />
- <video
- v-else-if="viewingReport.story.type === 'video'"
- height="140"
- controls
- :src="viewingReport.story.media_src"
- onerror="this.src='/storage/no-preview.png';this.onerror=null;"
- ></video>
- </div>
- </div>
- <div v-if="viewingReport && viewingReport.admin_seen_at === null" class="mt-4">
- <div v-if="viewingReport && viewingReport.object_type === 'App\\Profile'">
- <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('profile', 'ignore')">Ignore Report</button>
- <hr v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin" class="mt-3 mb-1">
- <div
- v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin"
- class="d-flex flex-row mt-2"
- style="gap:0.3rem;">
- <button
- class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
- @click="handleAction('profile', 'nsfw')">
- Mark all Posts NSFW
- </button>
- <button
- class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
- @click="handleAction('profile', 'unlist')">
- Unlist all Posts
- </button>
- </div>
- <button
- v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin"
- class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-2"
- @click="handleAction('profile', 'delete')">
- Delete Profile
- </button>
- </div>
- <div v-else-if="viewingReport && viewingReport.object_type === 'App\\Status'">
- <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('post', 'ignore')">Ignore Report</button>
- <hr v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin" class="mt-3 mb-1">
- <div
- v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin"
- class="d-flex flex-row mt-2"
- style="gap:0.3rem;">
- <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'nsfw')">Mark Post NSFW</button>
- <button v-if="viewingReport.status.visibility === 'public'" class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'unlist')">Unlist Post</button>
- <button v-else-if="viewingReport.status.visibility === 'unlisted'" class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'private')">Make Post Private</button>
- </div>
- <div
- v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin"
- class="d-flex flex-row mt-2"
- style="gap:0.3rem;">
- <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'nsfw')">Make all NSFW</button>
- <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'unlist')">Make all Unlisted</button>
- <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'private')">Make all Private</button>
- </div>
- <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
- <hr class="my-2">
- <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
- <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'delete')">Delete Post</button>
- <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'delete')">Delete Account</button>
- </div>
- </div>
- </div>
- <div v-else-if="viewingReport && viewingReport.object_type === 'App\\Story'">
- <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('story', 'ignore')">Ignore Report</button>
- <hr v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin" class="mt-3 mb-1">
- <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
- <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
- <button class="btn btn-danger btn-block rounded-pill mt-0" @click="handleAction('story', 'delete')">Delete Story</button>
- <button class="btn btn-outline-danger btn-block rounded-pill mt-0" @click="handleAction('story', 'delete-all')">Delete All Stories</button>
- </div>
- </div>
- <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
- <hr class="my-2">
- <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
- <button class="btn btn-outline-danger btn-sm btn-block rounded-pill mt-0" @click="handleAction('profile', 'delete')">Delete Account</button>
- </div>
- </div>
- </div>
- </div>
- </template>
- </b-modal>
- <b-modal v-model="showSpamReportModal" title="Potential Spam Post Detected" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
- <div v-if="viewingSpamReportLoading" class="d-flex align-items-center justify-content-center">
- <b-spinner />
- </div>
- <template v-else>
- <div class="list-group list-group-horizontal mt-3">
- <div v-if="viewingSpamReport && viewingSpamReport.status && viewingSpamReport.status.account" class="list-group-item d-flex align-items-center justify-content-between flex-column flex-grow-1" style="gap:0.4rem;">
- <div class="text-muted small font-weight-bold mt-n1">Reported Account</div>
- <a v-if="viewingSpamReport.status.account && viewingSpamReport.status.account.id" :href="`/i/web/profile/${viewingSpamReport.status.account.id}`" target="_blank" class="text-primary">
- <div class="d-flex align-items-center" style="gap:0.61rem;">
- <img
- :src="viewingSpamReport.status.account.avatar"
- width="30"
- height="30"
- style="object-fit: cover;border-radius:30px;"
- onerror="this.src='/storage/avatars/default.png';this.error=null;">
- <div class="d-flex flex-column">
- <p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;" :class="[ viewingSpamReport.status.account.is_admin ? 'text-danger': '']">@{{viewingSpamReport.status.account.acct}}</p>
- <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
- <span>{{viewingSpamReport.status.account.followers_count}} Followers</span>
- <span>·</span>
- <span>Joined {{ timeAgo(viewingSpamReport.status.account.created_at) }}</span>
- </div>
- </div>
- </div>
- </a>
- </div>
- </div>
- <div v-if="viewingSpamReport && viewingSpamReport.status" class="list-group mt-3">
- <div v-if="viewingSpamReport && viewingSpamReport.status && viewingSpamReport.status.media_attachments.length" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
- <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
- <div>Reported Post</div>
- <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
- </div>
- <img
- v-if="viewingSpamReport.status.media_attachments[0].type === 'image'"
- :src="viewingSpamReport.status.media_attachments[0].url"
- height="140"
- class="rounded"
- style="object-fit: cover;"
- onerror="this.src='/storage/no-preview.png';this.error=null;" />
- <video
- v-else-if="viewingSpamReport.status.media_attachments[0].type === 'video'"
- height="140"
- controls
- :src="viewingSpamReport.status.media_attachments[0].url"
- onerror="this.src='/storage/no-preview.png';this.onerror=null;"
- ></video>
- </div>
- <div
- v-if="viewingSpamReport &&
- viewingSpamReport.status &&
- viewingSpamReport.status.content_text &&
- viewingSpamReport.status.content_text.length"
- class="list-group-item d-flex flex-column flex-grow-1"
- style="gap:0.4rem;">
- <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
- <div>Reported Post Caption</div>
- <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
- </div>
- <p class="mb-0 read-more" style="font-size:12px;overflow-y: hidden">{{ viewingSpamReport.status.content_text }}</p>
- </div>
- </div>
- <div class="mt-4">
- <div>
- <button
- type="button"
- class="btn btn-dark btn-block rounded-pill"
- @click="handleSpamAction('mark-read')">
- Mark as Read
- </button>
- <button
- type="button"
- class="btn btn-danger btn-block rounded-pill"
- @click="handleSpamAction('mark-not-spam')">
- Mark As Not Spam
- </button>
- <hr class="mt-3 mb-1">
- <div
- class="d-flex flex-row mt-2"
- style="gap:0.3rem;">
- <button
- type="button"
- class="btn btn-dark btn-block btn-sm rounded-pill mt-0"
- @click="handleSpamAction('mark-all-read')">
- Mark All As Read
- </button>
- <button
- type="button"
- class="btn btn-dark btn-block btn-sm rounded-pill mt-0"
- @click="handleSpamAction('mark-all-not-spam')">
- Mark All As Not Spam
- </button>
- </div>
- <div>
- <hr class="my-2">
- <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
- <button
- type="button"
- class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
- @click="handleSpamAction('delete-profile')">
- Delete Account
- </button>
- </div>
- </div>
- </div>
- </div>
- </template>
- </b-modal>
- <template v-if="showRemoteReportModal">
- <admin-report-modal
- :open="showRemoteReportModal"
- :model="remoteReportModalModel"
- v-on:close="handleCloseRemoteReportModal()"
- v-on:refresh="refreshRemoteReports()" />
- </template>
- </div>
- </template>
- <script type="text/javascript">
- import AdminRemoteReportModal from "./partial/AdminRemoteReportModal.vue";
- export default {
- components: {
- "admin-report-modal": AdminRemoteReportModal
- },
- data() {
- return {
- loaded: false,
- stats: {
- total: 0,
- open: 0,
- closed: 0,
- autospam: 0,
- autospam_open: 0,
- remote_open: 0,
- },
- tabIndex: 0,
- reports: [],
- pagination: {},
- showReportModal: false,
- viewingReport: undefined,
- viewingReportLoading: false,
- autospam: [],
- autospamPagination: {},
- autospamLoaded: false,
- showSpamReportModal: false,
- viewingSpamReport: undefined,
- viewingSpamReportLoading: false,
- remoteReportsLoaded: false,
- showRemoteReportModal: undefined,
- remoteReportModalModel: {}
- }
- },
- mounted() {
- let u = new URLSearchParams(window.location.search);
- if(u.has('tab') && u.has('id') && u.get('tab') === 'autospam') {
- this.fetchStats(null, '/i/admin/api/reports/spam/all');
- this.fetchSpamReport(u.get('id'));
- } else if(u.has('tab') && u.has('id') && u.get('tab') === 'report') {
- this.fetchStats();
- this.fetchReport(u.get('id'));
- } else {
- window.history.pushState(null, null, '/i/admin/reports');
- this.fetchStats();
- }
- this.$root.$on('bv::modal::hide', (bvEvent, modalId) => {
- window.history.pushState(null, null, '/i/admin/reports');
- })
- },
- methods: {
- toggleTab(idx) {
- switch(idx) {
- case 0:
- this.fetchStats('/i/admin/api/reports/all');
- break;
- case 1:
- this.fetchStats('/i/admin/api/reports/all?filter=closed')
- break;
- case 2:
- this.fetchStats(null, '/i/admin/api/reports/spam/all');
- break;
- case 3:
- this.fetchRemoteReports();
- break;
- }
- window.history.pushState(null, null, '/i/admin/reports');
- this.tabIndex = idx;
- },
- prettyCount(str) {
- if(str) {
- return str.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
- }
- return str;
- },
- timeAgo(str) {
- if(!str) {
- return str;
- }
- return App.util.format.timeAgo(str);
- },
- formatDate(str) {
- let date = new Date(str);
- return new Intl.DateTimeFormat('default', {
- month: 'long',
- day: 'numeric',
- year: 'numeric',
- hour: 'numeric',
- minute: 'numeric'
- }).format(date);
- },
- reportLabel(report) {
- switch(report.object_type) {
- case 'App\\Profile':
- return `${report.type} Profile`;
- break;
- case 'App\\Status':
- return `${report.type} Post`;
- break;
- case 'App\\Story':
- return `${report.type} Story`;
- break;
- }
- },
- fetchStats(fetchReportsUrl = '/i/admin/api/reports/all', fetchSpamUrl = null) {
- axios.get('/i/admin/api/reports/stats')
- .then(res => {
- this.stats = res.data;
- })
- .finally(() => {
- if(fetchReportsUrl) {
- this.fetchReports(fetchReportsUrl);
- } else if(fetchSpamUrl) {
- this.fetchAutospam(fetchSpamUrl);
- }
- $('[data-toggle="tooltip"]').tooltip()
- });
- },
- fetchReports(url = '/i/admin/api/reports/all') {
- axios.get(url)
- .then(res => {
- this.reports = res.data.data;
- this.pagination = {
- next: res.data.links.next,
- prev: res.data.links.prev
- };
- })
- .finally(() => {
- this.loaded = true;
- });
- },
- fetchRemoteReports(url = '/i/admin/api/reports/remote') {
- axios.get(url)
- .then(res => {
- this.reports = res.data.data;
- this.pagination = {
- next: res.data.links.next,
- prev: res.data.links.prev
- };
- })
- .finally(() => {
- this.loaded = true;
- this.remoteReportsLoaded = true;
- });
- },
- remoteReportPaginate(dir) {
- event.currentTarget.blur();
- let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
- this.fetchRemoteReports(url);
- },
- handleCloseRemoteReportModal() {
- this.showRemoteReportModal = false;
- },
- showRemoteReport(report) {
- this.remoteReportModalModel = report;
- this.showRemoteReportModal = true;
- },
- refreshRemoteReports() {
- this.fetchStats('');
- this.$nextTick(() => {
- this.toggleTab(3);
- })
- },
- paginate(dir) {
- event.currentTarget.blur();
- let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
- this.fetchReports(url);
- },
- viewReport(report) {
- this.viewingReportLoading = false;
- this.viewingReport = report;
- this.showReportModal = true;
- window.history.pushState(null, null, '/i/admin/reports?tab=report&id=' + report.id);
- setTimeout(() => {
- pixelfed.readmore()
- }, 1000)
- },
- handleAction(type, action) {
- event.currentTarget.blur();
- this.viewingReportLoading = true;
- if(action !== 'ignore' && !window.confirm(this.getActionLabel(type, action))) {
- this.viewingReportLoading = false;
- return;
- }
- this.loaded = false;
- axios.post('/i/admin/api/reports/handle', {
- id: this.viewingReport.id,
- object_id: this.viewingReport.object_id,
- object_type: this.viewingReport.object_type,
- action: action,
- action_type: type
- })
- .catch(err => {
- swal('Error', err.response.data.error, 'error');
- })
- .finally(() => {
- this.viewingReportLoading = true;
- this.viewingReport = false;
- this.showReportModal = false;
- setTimeout(() => {
- this.fetchStats();
- }, 1000);
- })
- },
- getActionLabel(type, action) {
- if(type === 'profile') {
- switch(action) {
- case 'ignore':
- return 'Are you sure you want to ignore this profile report?';
- break;
- case 'nsfw':
- return 'Are you sure you want to mark this profile as NSFW?';
- break;
- case 'unlist':
- return 'Are you sure you want to mark all posts by this profile as unlisted?';
- break;
- case 'private':
- return 'Are you sure you want to mark all posts by this profile as private?';
- break;
- case 'delete':
- return 'Are you sure you want to delete this profile?';
- break;
- }
- } else if(type === 'post') {
- switch(action) {
- case 'ignore':
- return 'Are you sure you want to ignore this post report?';
- break;
- case 'nsfw':
- return 'Are you sure you want to mark this post as NSFW?';
- break;
- case 'unlist':
- return 'Are you sure you want to mark this post as unlisted?';
- break;
- case 'private':
- return 'Are you sure you want to mark this post as private?';
- break;
- case 'delete':
- return 'Are you sure you want to delete this post?';
- break;
- }
- } else if(type === 'story') {
- switch(action) {
- case 'ignore':
- return 'Are you sure you want to ignore this story report?';
- break;
- case 'delete':
- return 'Are you sure you want to delete this story?';
- break;
- case 'delete-all':
- return 'Are you sure you want to delete all stories by this account?';
- break;
- }
- }
- },
- fetchAutospam(url = '/i/admin/api/reports/spam/all') {
- axios.get(url)
- .then(res => {
- this.autospam = res.data.data;
- this.autospamPagination = {
- next: res.data.links.next,
- prev: res.data.links.prev
- }
- })
- .finally(() => {
- this.autospamLoaded = true;
- this.loaded = true;
- })
- },
- autospamPaginate(dir) {
- event.currentTarget.blur();
- let url = dir == 'next' ? this.autospamPagination.next : this.autospamPagination.prev;
- this.fetchAutospam(url);
- },
- viewSpamReport(report) {
- this.viewingSpamReportLoading = false;
- this.viewingSpamReport = report;
- this.showSpamReportModal = true;
- window.history.pushState(null, null, '/i/admin/reports?tab=autospam&id=' + report.id);
- setTimeout(() => {
- pixelfed.readmore()
- }, 1000)
- },
- getSpamActionLabel(action) {
- switch(action) {
- case 'mark-all-read':
- return 'Are you sure you want to mark all spam reports by this account as read?';
- break;
- case 'mark-all-not-spam':
- return 'Are you sure you want to mark all spam reports by this account as not spam?';
- break;
- case 'delete-profile':
- return 'Are you sure you want to delete this profile?';
- break;
- }
- },
- handleSpamAction(action) {
- event.currentTarget.blur();
- this.viewingSpamReportLoading = true;
- if(action !== 'mark-not-spam' && action !== 'mark-read' && !window.confirm(this.getSpamActionLabel(action))) {
- this.viewingSpamReportLoading = false;
- return;
- }
- this.loaded = false;
- axios.post('/i/admin/api/reports/spam/handle', {
- id: this.viewingSpamReport.id,
- action: action,
- })
- .catch(err => {
- swal('Error', err.response.data.error, 'error');
- })
- .finally(() => {
- this.viewingSpamReportLoading = true;
- this.viewingSpamReport = false;
- this.showSpamReportModal = false;
- setTimeout(() => {
- this.fetchStats(null, '/i/admin/api/reports/spam/all');
- }, 500);
- })
- },
- fetchReport(id) {
- axios.get('/i/admin/api/reports/get/' + id)
- .then(res => {
- this.tabIndex = 0;
- this.viewReport(res.data.data);
- })
- .catch(err => {
- this.fetchStats();
- window.history.pushState(null, null, '/i/admin/reports');
- })
- },
- fetchSpamReport(id) {
- axios.get('/i/admin/api/reports/spam/get/' + id)
- .then(res => {
- this.tabIndex = 2;
- this.viewSpamReport(res.data.data);
- })
- .catch(err => {
- this.fetchStats();
- window.history.pushState(null, null, '/i/admin/reports');
- })
- }
- }
- }
- </script>
|