AdminReports.vue 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  1. <template>
  2. <div>
  3. <div class="header bg-primary pb-3 mt-n4">
  4. <div class="container-fluid">
  5. <div class="header-body">
  6. <div class="row align-items-center py-4">
  7. <div class="col-lg-6 col-7">
  8. <p class="display-1 text-white d-inline-block mb-0">Moderation</p>
  9. </div>
  10. </div>
  11. <div class="row">
  12. <div class="col-12 col-sm-6 col-lg-3">
  13. <div class="mb-3">
  14. <h5 class="text-light text-uppercase mb-0">Active Reports</h5>
  15. <span
  16. class="text-white h2 font-weight-bold mb-0 human-size"
  17. data-toggle="tooltip"
  18. data-placement="bottom"
  19. :title="stats.open + ' open reports'">
  20. {{ prettyCount(stats.open) }}
  21. </span>
  22. </div>
  23. </div>
  24. <div class="col-12 col-sm-6 col-lg-3">
  25. <div class="mb-3">
  26. <h5 class="text-light text-uppercase mb-0">Active Spam Detections</h5>
  27. <span
  28. class="text-white h2 font-weight-bold mb-0 human-size"
  29. data-toggle="tooltip"
  30. data-placement="bottom"
  31. :title="stats.autospam_open + ' open spam detections'"
  32. >{{ prettyCount(stats.autospam_open) }}</span>
  33. </div>
  34. </div>
  35. <div class="col-12 col-sm-6 col-lg-3">
  36. <div class="mb-3">
  37. <h5 class="text-light text-uppercase mb-0">Total Reports</h5>
  38. <span
  39. class="text-white h2 font-weight-bold mb-0 human-size"
  40. data-toggle="tooltip"
  41. data-placement="bottom"
  42. :title="stats.total + ' total reports'"
  43. >{{ prettyCount(stats.total) }}
  44. </span>
  45. </div>
  46. </div>
  47. <div class="col-12 col-sm-6 col-lg-3">
  48. <div class="mb-3">
  49. <h5 class="text-light text-uppercase mb-0">Total Spam Detections</h5>
  50. <span
  51. class="text-white h2 font-weight-bold mb-0 human-size"
  52. data-toggle="tooltip"
  53. data-placement="bottom"
  54. :title="stats.autospam + ' total spam detections'">
  55. {{ prettyCount(stats.autospam) }}
  56. </span>
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. </div>
  62. </div>
  63. <div v-if="!loaded" class="my-5 text-center">
  64. <b-spinner />
  65. </div>
  66. <div v-else class="m-n2 m-lg-4">
  67. <div class="container-fluid mt-4">
  68. <div class="row mb-3 justify-content-between">
  69. <div class="col-12">
  70. <ul class="nav nav-pills">
  71. <li class="nav-item">
  72. <a
  73. :class="['nav-link d-flex align-items-center', { active: tabIndex == 0}]"
  74. href="#"
  75. @click.prevent="toggleTab(0)">
  76. <span>Open Reports</span>
  77. <span
  78. v-if="stats.open"
  79. class="badge badge-sm badge-floating badge-danger border-white ml-2"
  80. style="background-color: red;color:white;font-size:11px;">
  81. {{prettyCount(stats.open)}}
  82. </span>
  83. </a>
  84. </li>
  85. <li class="nav-item">
  86. <a
  87. :class="['nav-link d-flex align-items-center', { active: tabIndex == 2}]"
  88. href="#"
  89. @click.prevent="toggleTab(2)">
  90. <span>Spam Detections</span>
  91. <span
  92. v-if="stats.autospam_open"
  93. class="badge badge-sm badge-floating badge-danger border-white ml-2"
  94. style="background-color: red;color:white;font-size:11px;">
  95. {{prettyCount(stats.autospam_open)}}
  96. </span>
  97. </a>
  98. </li>
  99. <li class="d-none d-md-block nav-item">
  100. <a
  101. :class="['nav-link d-flex align-items-center', { active: tabIndex == 1}]"
  102. href="#"
  103. @click.prevent="toggleTab(1)">
  104. <span>Closed Reports</span>
  105. <span
  106. v-if="stats.autospam_open"
  107. class="badge badge-sm badge-floating badge-secondary border-white ml-2"
  108. style="font-size:11px;">
  109. {{prettyCount(stats.closed)}}
  110. </span>
  111. </a>
  112. </li>
  113. <li class="d-none d-md-block nav-item">
  114. <a
  115. href="/i/admin/reports/email-verifications"
  116. class="nav-link d-flex align-items-center">
  117. <span>Email Verification Requests</span>
  118. <span
  119. v-if="stats.email_verification_requests"
  120. class="badge badge-sm badge-floating badge-secondary border-white ml-2"
  121. style="font-size:11px;">
  122. {{prettyCount(stats.email_verification_requests)}}
  123. </span>
  124. </a>
  125. </li>
  126. <li class="d-none d-md-block nav-item">
  127. <a
  128. href="/i/admin/reports/appeals"
  129. class="nav-link d-flex align-items-center">
  130. <span>Appeal Requests</span>
  131. <span
  132. v-if="stats.appeals"
  133. class="badge badge-sm badge-floating badge-secondary border-white ml-2"
  134. style="font-size:11px;">
  135. {{ prettyCount(stats.appeals) }}
  136. </span>
  137. </a>
  138. </li>
  139. </ul>
  140. </div>
  141. </div>
  142. <div v-if="[0, 1].includes(this.tabIndex)" class="table-responsive rounded">
  143. <table v-if="reports && reports.length" class="table table-dark">
  144. <thead class="thead-dark">
  145. <tr>
  146. <th scope="col">ID</th>
  147. <th scope="col">Report</th>
  148. <th scope="col">Reported Account</th>
  149. <th scope="col">Reported By</th>
  150. <th scope="col">Created</th>
  151. <th scope="col">View Report</th>
  152. </tr>
  153. </thead>
  154. <tbody>
  155. <tr v-for="(report, idx) in reports">
  156. <td class="font-weight-bold text-monospace text-muted align-middle">
  157. <a href="#" @click.prevent="viewReport(report)">
  158. {{ report.id }}
  159. </a>
  160. </td>
  161. <td class="align-middle">
  162. <p class="text-capitalize font-weight-bold mb-0" v-html="reportLabel(report)"></p>
  163. </td>
  164. <td class="align-middle">
  165. <a v-if="report.reported && report.reported.id" :href="`/i/web/profile/${report.reported.id}`" target="_blank" class="text-white">
  166. <div class="d-flex align-items-center" style="gap:0.61rem;">
  167. <img
  168. :src="report.reported.avatar"
  169. width="30"
  170. height="30"
  171. style="object-fit: cover;border-radius:30px;"
  172. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  173. <div class="d-flex flex-column">
  174. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reported.username}}</p>
  175. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  176. <span>{{report.reported.followers_count}} Followers</span>
  177. <span>·</span>
  178. <span>Joined {{ timeAgo(report.reported.created_at) }}</span>
  179. </div>
  180. </div>
  181. </div>
  182. </a>
  183. </td>
  184. <td class="align-middle">
  185. <a :href="`/i/web/profile/${report.reporter.id}`" target="_blank" class="text-white">
  186. <div class="d-flex align-items-center" style="gap:0.61rem;">
  187. <img
  188. :src="report.reporter.avatar"
  189. width="30"
  190. height="30"
  191. style="object-fit: cover;border-radius:30px;"
  192. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  193. <div class="d-flex flex-column">
  194. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reporter.username}}</p>
  195. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  196. <span>{{report.reporter.followers_count}} Followers</span>
  197. <span>·</span>
  198. <span>Joined {{ timeAgo(report.reporter.created_at) }}</span>
  199. </div>
  200. </div>
  201. </div>
  202. </a>
  203. </td>
  204. <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
  205. <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="viewReport(report)">View</a></td>
  206. </tr>
  207. </tbody>
  208. </table>
  209. <div v-else>
  210. <div class="card card-body p-5">
  211. <div class="d-flex justify-content-between align-items-center flex-column">
  212. <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
  213. <p class="lead">{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}</p>
  214. </div>
  215. </div>
  216. </div>
  217. </div>
  218. <div v-if="[0, 1].includes(this.tabIndex) && reports.length && (pagination.prev || pagination.next)" class="d-flex align-items-center justify-content-center">
  219. <button
  220. class="btn btn-primary rounded-pill"
  221. :disabled="!pagination.prev"
  222. @click="paginate('prev')">
  223. Prev
  224. </button>
  225. <button
  226. class="btn btn-primary rounded-pill"
  227. :disabled="!pagination.next"
  228. @click="paginate('next')">
  229. Next
  230. </button>
  231. </div>
  232. <div v-if="this.tabIndex === 2" class="table-responsive rounded">
  233. <template v-if="autospamLoaded">
  234. <table v-if="autospam && autospam.length" class="table table-dark">
  235. <thead class="thead-dark">
  236. <tr>
  237. <th scope="col">ID</th>
  238. <th scope="col">Report</th>
  239. <th scope="col">Reported Account</th>
  240. <th scope="col">Created</th>
  241. <th scope="col">View Report</th>
  242. </tr>
  243. </thead>
  244. <tbody>
  245. <tr v-for="(report, idx) in autospam">
  246. <td class="font-weight-bold text-monospace text-muted align-middle">
  247. <a href="#" @click.prevent="viewSpamReport(report)">
  248. {{ report.id }}
  249. </a>
  250. </td>
  251. <td class="align-middle">
  252. <p class="text-capitalize font-weight-bold mb-0">Spam Post</p>
  253. </td>
  254. <td class="align-middle">
  255. <a v-if="report.status && report.status.account" :href="`/i/web/profile/${report.status.account.id}`" target="_blank" class="text-white">
  256. <div class="d-flex align-items-center" style="gap:0.61rem;">
  257. <img
  258. :src="report.status.account.avatar"
  259. width="30"
  260. height="30"
  261. style="object-fit: cover;border-radius:30px;"
  262. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  263. <div class="d-flex flex-column">
  264. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.status.account.username}}</p>
  265. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  266. <span>{{report.status.account.followers_count}} Followers</span>
  267. <span>·</span>
  268. <span>Joined {{ timeAgo(report.status.account.created_at) }}</span>
  269. </div>
  270. </div>
  271. </div>
  272. </a>
  273. </td>
  274. <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
  275. <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="viewSpamReport(report)">View</a></td>
  276. </tr>
  277. </tbody>
  278. </table>
  279. <div v-else>
  280. <div class="card card-body p-5">
  281. <div class="d-flex justify-content-between align-items-center flex-column">
  282. <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
  283. <p class="lead">No Spam Reports Found!</p>
  284. </div>
  285. </div>
  286. </div>
  287. </template>
  288. <div v-else class="d-flex align-items-center justify-content-center" style="min-height: 300px;">
  289. <b-spinner />
  290. </div>
  291. </div>
  292. <div v-if="this.tabIndex === 2 && autospamLoaded && autospam && autospam.length" class="d-flex align-items-center justify-content-center">
  293. <button
  294. class="btn btn-primary rounded-pill"
  295. :disabled="!autospamPagination.prev"
  296. @click="autospamPaginate('prev')">
  297. Prev
  298. </button>
  299. <button
  300. class="btn btn-primary rounded-pill"
  301. :disabled="!autospamPagination.next"
  302. @click="autospamPaginate('next')">
  303. Next
  304. </button>
  305. </div>
  306. </div>
  307. </div>
  308. <b-modal v-model="showReportModal" :title="tabIndex === 0 ? 'View Report' : 'Viewing Closed Report'" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
  309. <div v-if="viewingReportLoading" class="d-flex align-items-center justify-content-center">
  310. <b-spinner />
  311. </div>
  312. <template v-else>
  313. <div v-if="viewingReport" class="list-group">
  314. <div class="list-group-item d-flex align-items-center justify-content-between">
  315. <div class="text-muted small">Type</div>
  316. <div class="font-weight-bold text-capitalize" v-html="reportLabel(viewingReport)"></div>
  317. </div>
  318. <div v-if="viewingReport.admin_seen_at" class="list-group-item d-flex align-items-center justify-content-between">
  319. <div class="text-muted small">Report Closed</div>
  320. <div class="font-weight-bold text-capitalize">{{ formatDate(viewingReport.admin_seen_at) }}</div>
  321. </div>
  322. <div v-if="viewingReport.reporter_message" class="list-group-item d-flex flex-column" style="gap:10px;">
  323. <div class="text-muted small">Message</div>
  324. <p class="mb-0 read-more" style="font-size: 12px;overflow-y: hidden;">{{ viewingReport.reporter_message }}</p>
  325. </div>
  326. </div>
  327. <div class="list-group list-group-horizontal mt-3">
  328. <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;">
  329. <div class="text-muted small font-weight-bold mt-n1">Reported Account</div>
  330. <a v-if="viewingReport.reported && viewingReport.reported.id" :href="`/i/web/profile/${viewingReport.reported.id}`" target="_blank" class="text-primary">
  331. <div class="d-flex align-items-center" style="gap:0.61rem;">
  332. <img
  333. :src="viewingReport.reported.avatar"
  334. width="30"
  335. height="30"
  336. style="object-fit: cover;border-radius:30px;"
  337. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  338. <div class="d-flex flex-column">
  339. <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>
  340. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  341. <span>{{viewingReport.reported.followers_count}} Followers</span>
  342. <span>·</span>
  343. <span>Joined {{ timeAgo(viewingReport.reported.created_at) }}</span>
  344. </div>
  345. </div>
  346. </div>
  347. </a>
  348. </div>
  349. <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;">
  350. <div class="text-muted small font-weight-bold mt-n1">Reporter Account</div>
  351. <a v-if="viewingReport.reporter && viewingReport.reporter.id" :href="`/i/web/profile/${viewingReport.reporter.id}`" target="_blank" class="text-primary">
  352. <div class="d-flex align-items-center" style="gap:0.61rem;">
  353. <img
  354. :src="viewingReport.reporter.avatar"
  355. width="30"
  356. height="30"
  357. style="object-fit: cover;border-radius:30px;"
  358. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  359. <div class="d-flex flex-column">
  360. <p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;">@{{viewingReport.reporter.acct}}</p>
  361. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  362. <span>{{viewingReport.reporter.followers_count}} Followers</span>
  363. <span>·</span>
  364. <span>Joined {{ timeAgo(viewingReport.reporter.created_at) }}</span>
  365. </div>
  366. </div>
  367. </div>
  368. </a>
  369. </div>
  370. </div>
  371. <div v-if="viewingReport && viewingReport.object_type === 'App\\Status' && viewingReport.status" class="list-group mt-3">
  372. <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;">
  373. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  374. <div>Reported Post</div>
  375. <a class="font-weight-bold" :href="viewingReport.status.url" target="_blank">View</a>
  376. </div>
  377. <img
  378. v-if="viewingReport.status.media_attachments[0].type === 'image'"
  379. :src="viewingReport.status.media_attachments[0].url"
  380. height="140"
  381. class="rounded"
  382. style="object-fit: cover;"
  383. onerror="this.src='/storage/no-preview.png';this.error=null;" />
  384. <video
  385. v-else-if="viewingReport.status.media_attachments[0].type === 'video'"
  386. height="140"
  387. controls
  388. :src="viewingReport.status.media_attachments[0].url"
  389. onerror="this.src='/storage/no-preview.png';this.onerror=null;"
  390. ></video>
  391. </div>
  392. <div v-if="viewingReport && viewingReport.status" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
  393. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  394. <div>Reported Post Caption</div>
  395. <a class="font-weight-bold" :href="viewingReport.status.url" target="_blank">View</a>
  396. </div>
  397. <p class="mb-0 read-more" style="font-size:12px;overflow-y: hidden">{{ viewingReport.status.content_text }}</p>
  398. </div>
  399. </div>
  400. <div v-if="viewingReport && viewingReport.admin_seen_at === null" class="mt-4">
  401. <div v-if="viewingReport && viewingReport.object_type === 'App\\Profile'">
  402. <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('profile', 'ignore')">Ignore Report</button>
  403. <hr v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin" class="mt-3 mb-1">
  404. <div
  405. v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin"
  406. class="d-flex flex-row mt-2"
  407. style="gap:0.3rem;">
  408. <button
  409. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
  410. @click="handleAction('profile', 'nsfw')">
  411. Mark all Posts NSFW
  412. </button>
  413. <button
  414. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
  415. @click="handleAction('profile', 'unlist')">
  416. Unlist all Posts
  417. </button>
  418. </div>
  419. <button
  420. v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin"
  421. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-2"
  422. @click="handleAction('profile', 'delete')">
  423. Delete Profile
  424. </button>
  425. </div>
  426. <div v-if="viewingReport && viewingReport.object_type === 'App\\Status'">
  427. <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('post', 'ignore')">Ignore Report</button>
  428. <hr v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin" class="mt-3 mb-1">
  429. <div
  430. v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin"
  431. class="d-flex flex-row mt-2"
  432. style="gap:0.3rem;">
  433. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'nsfw')">Mark Post NSFW</button>
  434. <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>
  435. <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>
  436. </div>
  437. <div
  438. v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin"
  439. class="d-flex flex-row mt-2"
  440. style="gap:0.3rem;">
  441. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'nsfw')">Make all NSFW</button>
  442. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'unlist')">Make all Unlisted</button>
  443. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'private')">Make all Private</button>
  444. </div>
  445. <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
  446. <hr class="my-2">
  447. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  448. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'delete')">Delete Post</button>
  449. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'delete')">Delete Account</button>
  450. </div>
  451. </div>
  452. </div>
  453. </div>
  454. </template>
  455. </b-modal>
  456. <b-modal v-model="showSpamReportModal" title="Potential Spam Post Detected" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
  457. <div v-if="viewingSpamReportLoading" class="d-flex align-items-center justify-content-center">
  458. <b-spinner />
  459. </div>
  460. <template v-else>
  461. <div class="list-group list-group-horizontal mt-3">
  462. <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;">
  463. <div class="text-muted small font-weight-bold mt-n1">Reported Account</div>
  464. <a v-if="viewingSpamReport.status.account && viewingSpamReport.status.account.id" :href="`/i/web/profile/${viewingSpamReport.status.account.id}`" target="_blank" class="text-primary">
  465. <div class="d-flex align-items-center" style="gap:0.61rem;">
  466. <img
  467. :src="viewingSpamReport.status.account.avatar"
  468. width="30"
  469. height="30"
  470. style="object-fit: cover;border-radius:30px;"
  471. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  472. <div class="d-flex flex-column">
  473. <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>
  474. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  475. <span>{{viewingSpamReport.status.account.followers_count}} Followers</span>
  476. <span>·</span>
  477. <span>Joined {{ timeAgo(viewingSpamReport.status.account.created_at) }}</span>
  478. </div>
  479. </div>
  480. </div>
  481. </a>
  482. </div>
  483. </div>
  484. <div v-if="viewingSpamReport && viewingSpamReport.status" class="list-group mt-3">
  485. <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;">
  486. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  487. <div>Reported Post</div>
  488. <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
  489. </div>
  490. <img
  491. v-if="viewingSpamReport.status.media_attachments[0].type === 'image'"
  492. :src="viewingSpamReport.status.media_attachments[0].url"
  493. height="140"
  494. class="rounded"
  495. style="object-fit: cover;"
  496. onerror="this.src='/storage/no-preview.png';this.error=null;" />
  497. <video
  498. v-else-if="viewingSpamReport.status.media_attachments[0].type === 'video'"
  499. height="140"
  500. controls
  501. :src="viewingSpamReport.status.media_attachments[0].url"
  502. onerror="this.src='/storage/no-preview.png';this.onerror=null;"
  503. ></video>
  504. </div>
  505. <div
  506. v-if="viewingSpamReport &&
  507. viewingSpamReport.status &&
  508. viewingSpamReport.status.content_text &&
  509. viewingSpamReport.status.content_text.length"
  510. class="list-group-item d-flex flex-column flex-grow-1"
  511. style="gap:0.4rem;">
  512. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  513. <div>Reported Post Caption</div>
  514. <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
  515. </div>
  516. <p class="mb-0 read-more" style="font-size:12px;overflow-y: hidden">{{ viewingSpamReport.status.content_text }}</p>
  517. </div>
  518. </div>
  519. <div class="mt-4">
  520. <div>
  521. <button
  522. type="button"
  523. class="btn btn-dark btn-block rounded-pill"
  524. @click="handleSpamAction('mark-read')">
  525. Mark as Read
  526. </button>
  527. <button
  528. type="button"
  529. class="btn btn-danger btn-block rounded-pill"
  530. @click="handleSpamAction('mark-not-spam')">
  531. Mark As Not Spam
  532. </button>
  533. <hr class="mt-3 mb-1">
  534. <div
  535. class="d-flex flex-row mt-2"
  536. style="gap:0.3rem;">
  537. <button
  538. type="button"
  539. class="btn btn-dark btn-block btn-sm rounded-pill mt-0"
  540. @click="handleSpamAction('mark-all-read')">
  541. Mark All As Read
  542. </button>
  543. <button
  544. type="button"
  545. class="btn btn-dark btn-block btn-sm rounded-pill mt-0"
  546. @click="handleSpamAction('mark-all-not-spam')">
  547. Mark All As Not Spam
  548. </button>
  549. </div>
  550. <div>
  551. <hr class="my-2">
  552. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  553. <button
  554. type="button"
  555. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
  556. @click="handleSpamAction('delete-profile')">
  557. Delete Account
  558. </button>
  559. </div>
  560. </div>
  561. </div>
  562. </div>
  563. </template>
  564. </b-modal>
  565. </div>
  566. </template>
  567. <script type="text/javascript">
  568. export default {
  569. data() {
  570. return {
  571. loaded: false,
  572. stats: {
  573. total: 0,
  574. open: 0,
  575. closed: 0,
  576. autospam: 0,
  577. autospam_open: 0,
  578. },
  579. tabIndex: 0,
  580. reports: [],
  581. pagination: {},
  582. showReportModal: false,
  583. viewingReport: undefined,
  584. viewingReportLoading: false,
  585. autospam: [],
  586. autospamPagination: {},
  587. autospamLoaded: false,
  588. showSpamReportModal: false,
  589. viewingSpamReport: undefined,
  590. viewingSpamReportLoading: false
  591. }
  592. },
  593. mounted() {
  594. let u = new URLSearchParams(window.location.search);
  595. if(u.has('tab') && u.has('id') && u.get('tab') === 'autospam') {
  596. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  597. this.fetchSpamReport(u.get('id'));
  598. } else if(u.has('tab') && u.has('id') && u.get('tab') === 'report') {
  599. this.fetchStats();
  600. this.fetchReport(u.get('id'));
  601. } else {
  602. window.history.pushState(null, null, '/i/admin/reports');
  603. this.fetchStats();
  604. }
  605. this.$root.$on('bv::modal::hide', (bvEvent, modalId) => {
  606. window.history.pushState(null, null, '/i/admin/reports');
  607. })
  608. },
  609. methods: {
  610. toggleTab(idx) {
  611. switch(idx) {
  612. case 0:
  613. this.fetchStats('/i/admin/api/reports/all');
  614. break;
  615. case 1:
  616. this.fetchStats('/i/admin/api/reports/all?filter=closed')
  617. break;
  618. case 2:
  619. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  620. break;
  621. }
  622. window.history.pushState(null, null, '/i/admin/reports');
  623. this.tabIndex = idx;
  624. },
  625. prettyCount(str) {
  626. if(str) {
  627. return str.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
  628. }
  629. return str;
  630. },
  631. timeAgo(str) {
  632. if(!str) {
  633. return str;
  634. }
  635. return App.util.format.timeAgo(str);
  636. },
  637. formatDate(str) {
  638. let date = new Date(str);
  639. return new Intl.DateTimeFormat('default', {
  640. month: 'long',
  641. day: 'numeric',
  642. year: 'numeric',
  643. hour: 'numeric',
  644. minute: 'numeric'
  645. }).format(date);
  646. },
  647. reportLabel(report) {
  648. switch(report.object_type) {
  649. case 'App\\Profile':
  650. return `${report.type} Profile`;
  651. break;
  652. case 'App\\Status':
  653. return `${report.type} Post`;
  654. break;
  655. }
  656. },
  657. fetchStats(fetchReportsUrl = '/i/admin/api/reports/all', fetchSpamUrl = null) {
  658. axios.get('/i/admin/api/reports/stats')
  659. .then(res => {
  660. this.stats = res.data;
  661. })
  662. .finally(() => {
  663. if(fetchReportsUrl) {
  664. this.fetchReports(fetchReportsUrl);
  665. } else if(fetchSpamUrl) {
  666. this.fetchAutospam(fetchSpamUrl);
  667. }
  668. $('[data-toggle="tooltip"]').tooltip()
  669. });
  670. },
  671. fetchReports(url = '/i/admin/api/reports/all') {
  672. axios.get(url)
  673. .then(res => {
  674. this.reports = res.data.data;
  675. this.pagination = {
  676. next: res.data.links.next,
  677. prev: res.data.links.prev
  678. };
  679. })
  680. .finally(() => {
  681. this.loaded = true;
  682. });
  683. },
  684. paginate(dir) {
  685. event.currentTarget.blur();
  686. let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
  687. this.fetchReports(url);
  688. },
  689. viewReport(report) {
  690. this.viewingReportLoading = false;
  691. this.viewingReport = report;
  692. this.showReportModal = true;
  693. window.history.pushState(null, null, '/i/admin/reports?tab=report&id=' + report.id);
  694. setTimeout(() => {
  695. pixelfed.readmore()
  696. }, 1000)
  697. },
  698. handleAction(type, action) {
  699. event.currentTarget.blur();
  700. this.viewingReportLoading = true;
  701. if(action !== 'ignore' && !window.confirm(this.getActionLabel(type, action))) {
  702. this.viewingReportLoading = false;
  703. return;
  704. }
  705. this.loaded = false;
  706. axios.post('/i/admin/api/reports/handle', {
  707. id: this.viewingReport.id,
  708. object_id: this.viewingReport.object_id,
  709. object_type: this.viewingReport.object_type,
  710. action: action,
  711. action_type: type
  712. })
  713. .catch(err => {
  714. swal('Error', err.response.data.error, 'error');
  715. })
  716. .finally(() => {
  717. this.viewingReportLoading = true;
  718. this.viewingReport = false;
  719. this.showReportModal = false;
  720. setTimeout(() => {
  721. this.fetchStats();
  722. }, 1000);
  723. })
  724. },
  725. getActionLabel(type, action) {
  726. if(type === 'profile') {
  727. switch(action) {
  728. case 'ignore':
  729. return 'Are you sure you want to ignore this profile report?';
  730. break;
  731. case 'nsfw':
  732. return 'Are you sure you want to mark this profile as NSFW?';
  733. break;
  734. case 'unlist':
  735. return 'Are you sure you want to mark all posts by this profile as unlisted?';
  736. break;
  737. case 'private':
  738. return 'Are you sure you want to mark all posts by this profile as private?';
  739. break;
  740. case 'delete':
  741. return 'Are you sure you want to delete this profile?';
  742. break;
  743. }
  744. } else if(type === 'post') {
  745. switch(action) {
  746. case 'ignore':
  747. return 'Are you sure you want to ignore this post report?';
  748. break;
  749. case 'nsfw':
  750. return 'Are you sure you want to mark this post as NSFW?';
  751. break;
  752. case 'unlist':
  753. return 'Are you sure you want to mark this post as unlisted?';
  754. break;
  755. case 'private':
  756. return 'Are you sure you want to mark this post as private?';
  757. break;
  758. case 'delete':
  759. return 'Are you sure you want to delete this post?';
  760. break;
  761. }
  762. }
  763. },
  764. fetchAutospam(url = '/i/admin/api/reports/spam/all') {
  765. axios.get(url)
  766. .then(res => {
  767. this.autospam = res.data.data;
  768. this.autospamPagination = {
  769. next: res.data.links.next,
  770. prev: res.data.links.prev
  771. }
  772. })
  773. .finally(() => {
  774. this.autospamLoaded = true;
  775. this.loaded = true;
  776. })
  777. },
  778. autospamPaginate(dir) {
  779. event.currentTarget.blur();
  780. let url = dir == 'next' ? this.autospamPagination.next : this.autospamPagination.prev;
  781. this.fetchAutospam(url);
  782. },
  783. viewSpamReport(report) {
  784. this.viewingSpamReportLoading = false;
  785. this.viewingSpamReport = report;
  786. this.showSpamReportModal = true;
  787. window.history.pushState(null, null, '/i/admin/reports?tab=autospam&id=' + report.id);
  788. setTimeout(() => {
  789. pixelfed.readmore()
  790. }, 1000)
  791. },
  792. getSpamActionLabel(action) {
  793. switch(action) {
  794. case 'mark-all-read':
  795. return 'Are you sure you want to mark all spam reports by this account as read?';
  796. break;
  797. case 'mark-all-not-spam':
  798. return 'Are you sure you want to mark all spam reports by this account as not spam?';
  799. break;
  800. case 'delete-profile':
  801. return 'Are you sure you want to delete this profile?';
  802. break;
  803. }
  804. },
  805. handleSpamAction(action) {
  806. event.currentTarget.blur();
  807. this.viewingSpamReportLoading = true;
  808. if(action !== 'mark-not-spam' && action !== 'mark-read' && !window.confirm(this.getSpamActionLabel(action))) {
  809. this.viewingSpamReportLoading = false;
  810. return;
  811. }
  812. this.loaded = false;
  813. axios.post('/i/admin/api/reports/spam/handle', {
  814. id: this.viewingSpamReport.id,
  815. action: action,
  816. })
  817. .catch(err => {
  818. swal('Error', err.response.data.error, 'error');
  819. })
  820. .finally(() => {
  821. this.viewingSpamReportLoading = true;
  822. this.viewingSpamReport = false;
  823. this.showSpamReportModal = false;
  824. setTimeout(() => {
  825. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  826. }, 500);
  827. })
  828. },
  829. fetchReport(id) {
  830. axios.get('/i/admin/api/reports/get/' + id)
  831. .then(res => {
  832. this.tabIndex = 0;
  833. this.viewReport(res.data.data);
  834. })
  835. .catch(err => {
  836. this.fetchStats();
  837. window.history.pushState(null, null, '/i/admin/reports');
  838. })
  839. },
  840. fetchSpamReport(id) {
  841. axios.get('/i/admin/api/reports/spam/get/' + id)
  842. .then(res => {
  843. this.tabIndex = 2;
  844. this.viewSpamReport(res.data.data);
  845. })
  846. .catch(err => {
  847. this.fetchStats();
  848. window.history.pushState(null, null, '/i/admin/reports');
  849. })
  850. }
  851. }
  852. }
  853. </script>