AdminReports.vue 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155
  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="nav-item">
  100. <a
  101. :class="['nav-link d-flex align-items-center', { active: tabIndex == 3}]"
  102. href="#"
  103. @click.prevent="toggleTab(3)">
  104. <span>Remote Reports</span>
  105. <span
  106. v-if="stats.remote_open"
  107. class="badge badge-sm badge-floating badge-danger border-white ml-2"
  108. style="background-color: red;color:white;font-size:11px;">
  109. {{prettyCount(stats.remote_open)}}
  110. </span>
  111. </a>
  112. </li>
  113. <li class="d-none d-md-block nav-item">
  114. <a
  115. :class="['nav-link d-flex align-items-center', { active: tabIndex == 1}]"
  116. href="#"
  117. @click.prevent="toggleTab(1)">
  118. <span>Closed Reports</span>
  119. <span
  120. v-if="stats.autospam_open"
  121. class="badge badge-sm badge-floating badge-secondary border-white ml-2"
  122. style="font-size:11px;">
  123. {{prettyCount(stats.closed)}}
  124. </span>
  125. </a>
  126. </li>
  127. <li class="d-none d-md-block nav-item">
  128. <a
  129. href="/i/admin/reports/email-verifications"
  130. class="nav-link d-flex align-items-center">
  131. <span>Email Verification Requests</span>
  132. <span
  133. v-if="stats.email_verification_requests"
  134. class="badge badge-sm badge-floating badge-secondary border-white ml-2"
  135. style="font-size:11px;">
  136. {{prettyCount(stats.email_verification_requests)}}
  137. </span>
  138. </a>
  139. </li>
  140. <li class="d-none d-md-block nav-item">
  141. <a
  142. href="/i/admin/reports/appeals"
  143. class="nav-link d-flex align-items-center">
  144. <span>Appeal Requests</span>
  145. <span
  146. v-if="stats.appeals"
  147. class="badge badge-sm badge-floating badge-secondary border-white ml-2"
  148. style="font-size:11px;">
  149. {{ prettyCount(stats.appeals) }}
  150. </span>
  151. </a>
  152. </li>
  153. </ul>
  154. </div>
  155. </div>
  156. <div v-if="[0, 1].includes(this.tabIndex)" class="table-responsive rounded">
  157. <table v-if="reports && reports.length" class="table table-dark">
  158. <thead class="thead-dark">
  159. <tr>
  160. <th scope="col">ID</th>
  161. <th scope="col">Report</th>
  162. <th scope="col">Reported Account</th>
  163. <th scope="col">Reported By</th>
  164. <th scope="col">Created</th>
  165. <th scope="col">View Report</th>
  166. </tr>
  167. </thead>
  168. <tbody>
  169. <tr v-for="(report, idx) in reports">
  170. <td class="font-weight-bold text-monospace text-muted align-middle">
  171. <a href="#" @click.prevent="viewReport(report)">
  172. {{ report.id }}
  173. </a>
  174. </td>
  175. <td class="align-middle">
  176. <p class="text-capitalize font-weight-bold mb-0" v-html="reportLabel(report)"></p>
  177. </td>
  178. <td class="align-middle">
  179. <a v-if="report.reported && report.reported.id" :href="`/i/web/profile/${report.reported.id}`" target="_blank" class="text-white">
  180. <div class="d-flex align-items-center" style="gap:0.61rem;">
  181. <img
  182. :src="report.reported.avatar"
  183. width="30"
  184. height="30"
  185. style="object-fit: cover;border-radius:30px;"
  186. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  187. <div class="d-flex flex-column">
  188. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reported.username}}</p>
  189. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  190. <span>{{report.reported.followers_count}} Followers</span>
  191. <span>·</span>
  192. <span>Joined {{ timeAgo(report.reported.created_at) }}</span>
  193. </div>
  194. </div>
  195. </div>
  196. </a>
  197. </td>
  198. <td class="align-middle">
  199. <a
  200. v-if="report && report.reporter && report.reporter.id"
  201. :href="`/i/web/profile/${report.reporter.id}`"
  202. target="_blank"
  203. class="text-white">
  204. <div class="d-flex align-items-center" style="gap:0.61rem;">
  205. <img
  206. :src="report.reporter.avatar"
  207. width="30"
  208. height="30"
  209. style="object-fit: cover;border-radius:30px;"
  210. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  211. <div class="d-flex flex-column">
  212. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reporter.username}}</p>
  213. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  214. <span>{{report.reporter.followers_count}} Followers</span>
  215. <span>·</span>
  216. <span>Joined {{ timeAgo(report.reporter.created_at) }}</span>
  217. </div>
  218. </div>
  219. </div>
  220. </a>
  221. </td>
  222. <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
  223. <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="viewReport(report)">View</a></td>
  224. </tr>
  225. </tbody>
  226. </table>
  227. <div v-else>
  228. <div class="card card-body p-5">
  229. <div class="d-flex justify-content-between align-items-center flex-column">
  230. <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
  231. <p class="lead">{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}</p>
  232. </div>
  233. </div>
  234. </div>
  235. </div>
  236. <div v-if="[0, 1].includes(this.tabIndex) && reports.length && (pagination.prev || pagination.next)" class="d-flex align-items-center justify-content-center">
  237. <button
  238. class="btn btn-primary rounded-pill"
  239. :disabled="!pagination.prev"
  240. @click="paginate('prev')">
  241. Prev
  242. </button>
  243. <button
  244. class="btn btn-primary rounded-pill"
  245. :disabled="!pagination.next"
  246. @click="paginate('next')">
  247. Next
  248. </button>
  249. </div>
  250. <div v-if="this.tabIndex === 2" class="table-responsive rounded">
  251. <template v-if="autospamLoaded">
  252. <table v-if="autospam && autospam.length" class="table table-dark">
  253. <thead class="thead-dark">
  254. <tr>
  255. <th scope="col">ID</th>
  256. <th scope="col">Report</th>
  257. <th scope="col">Reported Account</th>
  258. <th scope="col">Created</th>
  259. <th scope="col">View Report</th>
  260. </tr>
  261. </thead>
  262. <tbody>
  263. <tr v-for="(report, idx) in autospam">
  264. <td class="font-weight-bold text-monospace text-muted align-middle">
  265. <a href="#" @click.prevent="viewSpamReport(report)">
  266. {{ report.id }}
  267. </a>
  268. </td>
  269. <td class="align-middle">
  270. <p class="text-capitalize font-weight-bold mb-0">Spam Post</p>
  271. </td>
  272. <td class="align-middle">
  273. <a v-if="report.status && report.status.account" :href="`/i/web/profile/${report.status.account.id}`" target="_blank" class="text-white">
  274. <div class="d-flex align-items-center" style="gap:0.61rem;">
  275. <img
  276. :src="report.status.account.avatar"
  277. width="30"
  278. height="30"
  279. style="object-fit: cover;border-radius:30px;"
  280. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  281. <div class="d-flex flex-column">
  282. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.status.account.username}}</p>
  283. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  284. <span>{{report.status.account.followers_count}} Followers</span>
  285. <span>·</span>
  286. <span>Joined {{ timeAgo(report.status.account.created_at) }}</span>
  287. </div>
  288. </div>
  289. </div>
  290. </a>
  291. </td>
  292. <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
  293. <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="viewSpamReport(report)">View</a></td>
  294. </tr>
  295. </tbody>
  296. </table>
  297. <div v-else>
  298. <div class="card card-body p-5">
  299. <div class="d-flex justify-content-between align-items-center flex-column">
  300. <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
  301. <p class="lead">No Spam Reports Found!</p>
  302. </div>
  303. </div>
  304. </div>
  305. </template>
  306. <div v-else class="d-flex align-items-center justify-content-center" style="min-height: 300px;">
  307. <b-spinner />
  308. </div>
  309. </div>
  310. <div v-if="this.tabIndex === 2 && autospamLoaded && autospam && autospam.length" class="d-flex align-items-center justify-content-center">
  311. <button
  312. class="btn btn-primary rounded-pill"
  313. :disabled="!autospamPagination.prev"
  314. @click="autospamPaginate('prev')">
  315. Prev
  316. </button>
  317. <button
  318. class="btn btn-primary rounded-pill"
  319. :disabled="!autospamPagination.next"
  320. @click="autospamPaginate('next')">
  321. Next
  322. </button>
  323. </div>
  324. <div v-if="this.tabIndex === 3" class="table-responsive rounded">
  325. <table v-if="reports && reports.length" class="table table-dark">
  326. <thead class="thead-dark">
  327. <tr>
  328. <th scope="col">ID</th>
  329. <th scope="col">Instance</th>
  330. <th scope="col">Reported Account</th>
  331. <th scope="col">Comment</th>
  332. <th scope="col">Created</th>
  333. <th scope="col">View Report</th>
  334. </tr>
  335. </thead>
  336. <tbody>
  337. <tr
  338. v-for="(report, idx) in reports"
  339. :key="`remote-reports-${report.id}-${idx}`">
  340. <td class="font-weight-bold text-monospace text-muted align-middle">
  341. <a href="#" @click.prevent="showRemoteReport(report)">
  342. {{ report.id }}
  343. </a>
  344. </td>
  345. <td class="align-middle">
  346. <p class="font-weight-bold mb-0">{{ report.instance }}</p>
  347. </td>
  348. <td class="align-middle">
  349. <a v-if="report.reported && report.reported.id" :href="`/i/web/profile/${report.reported.id}`" target="_blank" class="text-white">
  350. <div class="d-flex align-items-center" style="gap:0.61rem;">
  351. <img
  352. :src="report.reported.avatar"
  353. width="30"
  354. height="30"
  355. style="object-fit: cover;border-radius:30px;"
  356. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  357. <div class="d-flex flex-column">
  358. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reported.username}}</p>
  359. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  360. <span>{{report.reported.followers_count}} Followers</span>
  361. <span>·</span>
  362. <span>Joined {{ timeAgo(report.reported.created_at) }}</span>
  363. </div>
  364. </div>
  365. </div>
  366. </a>
  367. </td>
  368. <td class="align-middle">
  369. <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>
  370. </td>
  371. <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
  372. <td class="align-middle"><a href="#" class="btn btn-primary btn-sm">View</a></td>
  373. </tr>
  374. </tbody>
  375. </table>
  376. <div v-else>
  377. <div class="card card-body p-5">
  378. <div class="d-flex justify-content-between align-items-center flex-column">
  379. <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
  380. <p class="lead">{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}</p>
  381. </div>
  382. </div>
  383. </div>
  384. </div>
  385. <div v-if="this.tabIndex === 3 && remoteReportsLoaded && reports && reports.length" class="d-flex align-items-center justify-content-center">
  386. <button
  387. class="btn btn-primary rounded-pill"
  388. :disabled="!pagination.prev"
  389. @click="remoteReportPaginate('prev')">
  390. Prev
  391. </button>
  392. <button
  393. class="btn btn-primary rounded-pill"
  394. :disabled="!pagination.next"
  395. @click="remoteReportPaginate('next')">
  396. Next
  397. </button>
  398. </div>
  399. </div>
  400. </div>
  401. <b-modal v-model="showReportModal" :title="tabIndex === 0 ? 'View Report' : 'Viewing Closed Report'" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
  402. <div v-if="viewingReportLoading" class="d-flex align-items-center justify-content-center">
  403. <b-spinner />
  404. </div>
  405. <template v-else>
  406. <div v-if="viewingReport" class="list-group">
  407. <div class="list-group-item d-flex align-items-center justify-content-between">
  408. <div class="text-muted small">Type</div>
  409. <div class="font-weight-bold text-capitalize" v-html="reportLabel(viewingReport)"></div>
  410. </div>
  411. <div v-if="viewingReport.admin_seen_at" class="list-group-item d-flex align-items-center justify-content-between">
  412. <div class="text-muted small">Report Closed</div>
  413. <div class="font-weight-bold text-capitalize">{{ formatDate(viewingReport.admin_seen_at) }}</div>
  414. </div>
  415. <div v-if="viewingReport.reporter_message" class="list-group-item d-flex flex-column" style="gap:10px;">
  416. <div class="text-muted small">Message</div>
  417. <p class="mb-0 read-more" style="font-size: 12px;overflow-y: hidden;">{{ viewingReport.reporter_message }}</p>
  418. </div>
  419. </div>
  420. <div class="list-group list-group-horizontal mt-3">
  421. <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;">
  422. <div class="text-muted small font-weight-bold mt-n1">Reported Account</div>
  423. <a v-if="viewingReport.reported && viewingReport.reported.id" :href="`/i/web/profile/${viewingReport.reported.id}`" target="_blank" class="text-primary">
  424. <div class="d-flex align-items-center" style="gap:0.61rem;">
  425. <img
  426. :src="viewingReport.reported.avatar"
  427. width="30"
  428. height="30"
  429. style="object-fit: cover;border-radius:30px;"
  430. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  431. <div class="d-flex flex-column">
  432. <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>
  433. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  434. <span>{{viewingReport.reported.followers_count}} Followers</span>
  435. <span>·</span>
  436. <span>Joined {{ timeAgo(viewingReport.reported.created_at) }}</span>
  437. </div>
  438. </div>
  439. </div>
  440. </a>
  441. </div>
  442. <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;">
  443. <div class="text-muted small font-weight-bold mt-n1">Reporter Account</div>
  444. <a v-if="viewingReport.reporter && viewingReport.reporter?.id" :href="`/i/web/profile/${viewingReport.reporter?.id}`" target="_blank" class="text-primary">
  445. <div class="d-flex align-items-center" style="gap:0.61rem;">
  446. <img
  447. :src="viewingReport.reporter.avatar"
  448. width="30"
  449. height="30"
  450. style="object-fit: cover;border-radius:30px;"
  451. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  452. <div class="d-flex flex-column">
  453. <p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;">@{{viewingReport.reporter.acct}}</p>
  454. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  455. <span>{{viewingReport.reporter.followers_count}} Followers</span>
  456. <span>·</span>
  457. <span>Joined {{ timeAgo(viewingReport.reporter.created_at) }}</span>
  458. </div>
  459. </div>
  460. </div>
  461. </a>
  462. </div>
  463. </div>
  464. <div v-if="viewingReport && viewingReport.object_type === 'App\\Status' && viewingReport.status" class="list-group mt-3">
  465. <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;">
  466. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  467. <div>Reported Post</div>
  468. <a class="font-weight-bold" :href="viewingReport.status.url" target="_blank">View</a>
  469. </div>
  470. <img
  471. v-if="viewingReport.status.media_attachments[0].type === 'image'"
  472. :src="viewingReport.status.media_attachments[0].url"
  473. height="140"
  474. class="rounded"
  475. style="object-fit: cover;"
  476. onerror="this.src='/storage/no-preview.png';this.error=null;" />
  477. <video
  478. v-else-if="viewingReport.status.media_attachments[0].type === 'video'"
  479. height="140"
  480. controls
  481. :src="viewingReport.status.media_attachments[0].url"
  482. onerror="this.src='/storage/no-preview.png';this.onerror=null;"
  483. ></video>
  484. </div>
  485. <div v-if="viewingReport && viewingReport.status" 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 Caption</div>
  488. <a class="font-weight-bold" :href="viewingReport.status.url" target="_blank">View</a>
  489. </div>
  490. <p class="mb-0 read-more" style="font-size:12px;overflow-y: hidden">{{ viewingReport.status.content_text }}</p>
  491. </div>
  492. </div>
  493. <div v-else-if="viewingReport && viewingReport.object_type === 'App\\Story' && viewingReport.story" class="list-group mt-3">
  494. <div v-if="viewingReport && viewingReport.story" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
  495. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  496. <div>Reported Story</div>
  497. <a class="font-weight-bold" :href="viewingReport.story.url" target="_blank">View</a>
  498. </div>
  499. <img
  500. v-if="viewingReport.story.type === 'photo'"
  501. :src="viewingReport.story.media_src"
  502. height="140"
  503. class="rounded"
  504. style="object-fit: cover;"
  505. onerror="this.src='/storage/no-preview.png';this.error=null;" />
  506. <video
  507. v-else-if="viewingReport.story.type === 'video'"
  508. height="140"
  509. controls
  510. :src="viewingReport.story.media_src"
  511. onerror="this.src='/storage/no-preview.png';this.onerror=null;"
  512. ></video>
  513. </div>
  514. </div>
  515. <div v-if="viewingReport && viewingReport.admin_seen_at === null" class="mt-4">
  516. <div v-if="viewingReport && viewingReport.object_type === 'App\\Profile'">
  517. <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('profile', 'ignore')">Ignore Report</button>
  518. <hr v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin" class="mt-3 mb-1">
  519. <div
  520. v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin"
  521. class="d-flex flex-row mt-2"
  522. style="gap:0.3rem;">
  523. <button
  524. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
  525. @click="handleAction('profile', 'nsfw')">
  526. Mark all Posts NSFW
  527. </button>
  528. <button
  529. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
  530. @click="handleAction('profile', 'unlist')">
  531. Unlist all Posts
  532. </button>
  533. </div>
  534. <button
  535. v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin"
  536. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-2"
  537. @click="handleAction('profile', 'delete')">
  538. Delete Profile
  539. </button>
  540. </div>
  541. <div v-else-if="viewingReport && viewingReport.object_type === 'App\\Status'">
  542. <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('post', 'ignore')">Ignore Report</button>
  543. <hr v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin" class="mt-3 mb-1">
  544. <div
  545. v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin"
  546. class="d-flex flex-row mt-2"
  547. style="gap:0.3rem;">
  548. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'nsfw')">Mark Post NSFW</button>
  549. <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>
  550. <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>
  551. </div>
  552. <div
  553. v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin"
  554. class="d-flex flex-row mt-2"
  555. style="gap:0.3rem;">
  556. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'nsfw')">Make all NSFW</button>
  557. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'unlist')">Make all Unlisted</button>
  558. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'private')">Make all Private</button>
  559. </div>
  560. <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
  561. <hr class="my-2">
  562. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  563. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'delete')">Delete Post</button>
  564. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'delete')">Delete Account</button>
  565. </div>
  566. </div>
  567. </div>
  568. <div v-else-if="viewingReport && viewingReport.object_type === 'App\\Story'">
  569. <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('story', 'ignore')">Ignore Report</button>
  570. <hr v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin" class="mt-3 mb-1">
  571. <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
  572. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  573. <button class="btn btn-danger btn-block rounded-pill mt-0" @click="handleAction('story', 'delete')">Delete Story</button>
  574. <button class="btn btn-outline-danger btn-block rounded-pill mt-0" @click="handleAction('story', 'delete-all')">Delete All Stories</button>
  575. </div>
  576. </div>
  577. <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
  578. <hr class="my-2">
  579. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  580. <button class="btn btn-outline-danger btn-sm btn-block rounded-pill mt-0" @click="handleAction('profile', 'delete')">Delete Account</button>
  581. </div>
  582. </div>
  583. </div>
  584. </div>
  585. </template>
  586. </b-modal>
  587. <b-modal v-model="showSpamReportModal" title="Potential Spam Post Detected" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
  588. <div v-if="viewingSpamReportLoading" class="d-flex align-items-center justify-content-center">
  589. <b-spinner />
  590. </div>
  591. <template v-else>
  592. <div class="list-group list-group-horizontal mt-3">
  593. <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;">
  594. <div class="text-muted small font-weight-bold mt-n1">Reported Account</div>
  595. <a v-if="viewingSpamReport.status.account && viewingSpamReport.status.account.id" :href="`/i/web/profile/${viewingSpamReport.status.account.id}`" target="_blank" class="text-primary">
  596. <div class="d-flex align-items-center" style="gap:0.61rem;">
  597. <img
  598. :src="viewingSpamReport.status.account.avatar"
  599. width="30"
  600. height="30"
  601. style="object-fit: cover;border-radius:30px;"
  602. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  603. <div class="d-flex flex-column">
  604. <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>
  605. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  606. <span>{{viewingSpamReport.status.account.followers_count}} Followers</span>
  607. <span>·</span>
  608. <span>Joined {{ timeAgo(viewingSpamReport.status.account.created_at) }}</span>
  609. </div>
  610. </div>
  611. </div>
  612. </a>
  613. </div>
  614. </div>
  615. <div v-if="viewingSpamReport && viewingSpamReport.status" class="list-group mt-3">
  616. <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;">
  617. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  618. <div>Reported Post</div>
  619. <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
  620. </div>
  621. <img
  622. v-if="viewingSpamReport.status.media_attachments[0].type === 'image'"
  623. :src="viewingSpamReport.status.media_attachments[0].url"
  624. height="140"
  625. class="rounded"
  626. style="object-fit: cover;"
  627. onerror="this.src='/storage/no-preview.png';this.error=null;" />
  628. <video
  629. v-else-if="viewingSpamReport.status.media_attachments[0].type === 'video'"
  630. height="140"
  631. controls
  632. :src="viewingSpamReport.status.media_attachments[0].url"
  633. onerror="this.src='/storage/no-preview.png';this.onerror=null;"
  634. ></video>
  635. </div>
  636. <div
  637. v-if="viewingSpamReport &&
  638. viewingSpamReport.status &&
  639. viewingSpamReport.status.content_text &&
  640. viewingSpamReport.status.content_text.length"
  641. class="list-group-item d-flex flex-column flex-grow-1"
  642. style="gap:0.4rem;">
  643. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  644. <div>Reported Post Caption</div>
  645. <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
  646. </div>
  647. <p class="mb-0 read-more" style="font-size:12px;overflow-y: hidden">{{ viewingSpamReport.status.content_text }}</p>
  648. </div>
  649. </div>
  650. <div class="mt-4">
  651. <div>
  652. <button
  653. type="button"
  654. class="btn btn-dark btn-block rounded-pill"
  655. @click="handleSpamAction('mark-read')">
  656. Mark as Read
  657. </button>
  658. <button
  659. type="button"
  660. class="btn btn-danger btn-block rounded-pill"
  661. @click="handleSpamAction('mark-not-spam')">
  662. Mark As Not Spam
  663. </button>
  664. <hr class="mt-3 mb-1">
  665. <div
  666. class="d-flex flex-row mt-2"
  667. style="gap:0.3rem;">
  668. <button
  669. type="button"
  670. class="btn btn-dark btn-block btn-sm rounded-pill mt-0"
  671. @click="handleSpamAction('mark-all-read')">
  672. Mark All As Read
  673. </button>
  674. <button
  675. type="button"
  676. class="btn btn-dark btn-block btn-sm rounded-pill mt-0"
  677. @click="handleSpamAction('mark-all-not-spam')">
  678. Mark All As Not Spam
  679. </button>
  680. </div>
  681. <div>
  682. <hr class="my-2">
  683. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  684. <button
  685. type="button"
  686. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
  687. @click="handleSpamAction('delete-profile')">
  688. Delete Account
  689. </button>
  690. </div>
  691. </div>
  692. </div>
  693. </div>
  694. </template>
  695. </b-modal>
  696. <template v-if="showRemoteReportModal">
  697. <admin-report-modal
  698. :open="showRemoteReportModal"
  699. :model="remoteReportModalModel"
  700. v-on:close="handleCloseRemoteReportModal()"
  701. v-on:refresh="refreshRemoteReports()" />
  702. </template>
  703. </div>
  704. </template>
  705. <script type="text/javascript">
  706. import AdminRemoteReportModal from "./partial/AdminRemoteReportModal.vue";
  707. export default {
  708. components: {
  709. "admin-report-modal": AdminRemoteReportModal
  710. },
  711. data() {
  712. return {
  713. loaded: false,
  714. stats: {
  715. total: 0,
  716. open: 0,
  717. closed: 0,
  718. autospam: 0,
  719. autospam_open: 0,
  720. remote_open: 0,
  721. },
  722. tabIndex: 0,
  723. reports: [],
  724. pagination: {},
  725. showReportModal: false,
  726. viewingReport: undefined,
  727. viewingReportLoading: false,
  728. autospam: [],
  729. autospamPagination: {},
  730. autospamLoaded: false,
  731. showSpamReportModal: false,
  732. viewingSpamReport: undefined,
  733. viewingSpamReportLoading: false,
  734. remoteReportsLoaded: false,
  735. showRemoteReportModal: undefined,
  736. remoteReportModalModel: {}
  737. }
  738. },
  739. mounted() {
  740. let u = new URLSearchParams(window.location.search);
  741. if(u.has('tab') && u.has('id') && u.get('tab') === 'autospam') {
  742. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  743. this.fetchSpamReport(u.get('id'));
  744. } else if(u.has('tab') && u.has('id') && u.get('tab') === 'report') {
  745. this.fetchStats();
  746. this.fetchReport(u.get('id'));
  747. } else {
  748. window.history.pushState(null, null, '/i/admin/reports');
  749. this.fetchStats();
  750. }
  751. this.$root.$on('bv::modal::hide', (bvEvent, modalId) => {
  752. window.history.pushState(null, null, '/i/admin/reports');
  753. })
  754. },
  755. methods: {
  756. toggleTab(idx) {
  757. switch(idx) {
  758. case 0:
  759. this.fetchStats('/i/admin/api/reports/all');
  760. break;
  761. case 1:
  762. this.fetchStats('/i/admin/api/reports/all?filter=closed')
  763. break;
  764. case 2:
  765. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  766. break;
  767. case 3:
  768. this.fetchRemoteReports();
  769. break;
  770. }
  771. window.history.pushState(null, null, '/i/admin/reports');
  772. this.tabIndex = idx;
  773. },
  774. prettyCount(str) {
  775. if(str) {
  776. return str.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
  777. }
  778. return str;
  779. },
  780. timeAgo(str) {
  781. if(!str) {
  782. return str;
  783. }
  784. return App.util.format.timeAgo(str);
  785. },
  786. formatDate(str) {
  787. let date = new Date(str);
  788. return new Intl.DateTimeFormat('default', {
  789. month: 'long',
  790. day: 'numeric',
  791. year: 'numeric',
  792. hour: 'numeric',
  793. minute: 'numeric'
  794. }).format(date);
  795. },
  796. reportLabel(report) {
  797. switch(report.object_type) {
  798. case 'App\\Profile':
  799. return `${report.type} Profile`;
  800. break;
  801. case 'App\\Status':
  802. return `${report.type} Post`;
  803. break;
  804. case 'App\\Story':
  805. return `${report.type} Story`;
  806. break;
  807. }
  808. },
  809. fetchStats(fetchReportsUrl = '/i/admin/api/reports/all', fetchSpamUrl = null) {
  810. axios.get('/i/admin/api/reports/stats')
  811. .then(res => {
  812. this.stats = res.data;
  813. })
  814. .finally(() => {
  815. if(fetchReportsUrl) {
  816. this.fetchReports(fetchReportsUrl);
  817. } else if(fetchSpamUrl) {
  818. this.fetchAutospam(fetchSpamUrl);
  819. }
  820. $('[data-toggle="tooltip"]').tooltip()
  821. });
  822. },
  823. fetchReports(url = '/i/admin/api/reports/all') {
  824. axios.get(url)
  825. .then(res => {
  826. this.reports = res.data.data;
  827. this.pagination = {
  828. next: res.data.links.next,
  829. prev: res.data.links.prev
  830. };
  831. })
  832. .finally(() => {
  833. this.loaded = true;
  834. });
  835. },
  836. fetchRemoteReports(url = '/i/admin/api/reports/remote') {
  837. axios.get(url)
  838. .then(res => {
  839. this.reports = res.data.data;
  840. this.pagination = {
  841. next: res.data.links.next,
  842. prev: res.data.links.prev
  843. };
  844. })
  845. .finally(() => {
  846. this.loaded = true;
  847. this.remoteReportsLoaded = true;
  848. });
  849. },
  850. remoteReportPaginate(dir) {
  851. event.currentTarget.blur();
  852. let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
  853. this.fetchRemoteReports(url);
  854. },
  855. handleCloseRemoteReportModal() {
  856. this.showRemoteReportModal = false;
  857. },
  858. showRemoteReport(report) {
  859. this.remoteReportModalModel = report;
  860. this.showRemoteReportModal = true;
  861. },
  862. refreshRemoteReports() {
  863. this.fetchStats('');
  864. this.$nextTick(() => {
  865. this.toggleTab(3);
  866. })
  867. },
  868. paginate(dir) {
  869. event.currentTarget.blur();
  870. let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
  871. this.fetchReports(url);
  872. },
  873. viewReport(report) {
  874. this.viewingReportLoading = false;
  875. this.viewingReport = report;
  876. this.showReportModal = true;
  877. window.history.pushState(null, null, '/i/admin/reports?tab=report&id=' + report.id);
  878. setTimeout(() => {
  879. pixelfed.readmore()
  880. }, 1000)
  881. },
  882. handleAction(type, action) {
  883. event.currentTarget.blur();
  884. this.viewingReportLoading = true;
  885. if(action !== 'ignore' && !window.confirm(this.getActionLabel(type, action))) {
  886. this.viewingReportLoading = false;
  887. return;
  888. }
  889. this.loaded = false;
  890. axios.post('/i/admin/api/reports/handle', {
  891. id: this.viewingReport.id,
  892. object_id: this.viewingReport.object_id,
  893. object_type: this.viewingReport.object_type,
  894. action: action,
  895. action_type: type
  896. })
  897. .catch(err => {
  898. swal('Error', err.response.data.error, 'error');
  899. })
  900. .finally(() => {
  901. this.viewingReportLoading = true;
  902. this.viewingReport = false;
  903. this.showReportModal = false;
  904. setTimeout(() => {
  905. this.fetchStats();
  906. }, 1000);
  907. })
  908. },
  909. getActionLabel(type, action) {
  910. if(type === 'profile') {
  911. switch(action) {
  912. case 'ignore':
  913. return 'Are you sure you want to ignore this profile report?';
  914. break;
  915. case 'nsfw':
  916. return 'Are you sure you want to mark this profile as NSFW?';
  917. break;
  918. case 'unlist':
  919. return 'Are you sure you want to mark all posts by this profile as unlisted?';
  920. break;
  921. case 'private':
  922. return 'Are you sure you want to mark all posts by this profile as private?';
  923. break;
  924. case 'delete':
  925. return 'Are you sure you want to delete this profile?';
  926. break;
  927. }
  928. } else if(type === 'post') {
  929. switch(action) {
  930. case 'ignore':
  931. return 'Are you sure you want to ignore this post report?';
  932. break;
  933. case 'nsfw':
  934. return 'Are you sure you want to mark this post as NSFW?';
  935. break;
  936. case 'unlist':
  937. return 'Are you sure you want to mark this post as unlisted?';
  938. break;
  939. case 'private':
  940. return 'Are you sure you want to mark this post as private?';
  941. break;
  942. case 'delete':
  943. return 'Are you sure you want to delete this post?';
  944. break;
  945. }
  946. } else if(type === 'story') {
  947. switch(action) {
  948. case 'ignore':
  949. return 'Are you sure you want to ignore this story report?';
  950. break;
  951. case 'delete':
  952. return 'Are you sure you want to delete this story?';
  953. break;
  954. case 'delete-all':
  955. return 'Are you sure you want to delete all stories by this account?';
  956. break;
  957. }
  958. }
  959. },
  960. fetchAutospam(url = '/i/admin/api/reports/spam/all') {
  961. axios.get(url)
  962. .then(res => {
  963. this.autospam = res.data.data;
  964. this.autospamPagination = {
  965. next: res.data.links.next,
  966. prev: res.data.links.prev
  967. }
  968. })
  969. .finally(() => {
  970. this.autospamLoaded = true;
  971. this.loaded = true;
  972. })
  973. },
  974. autospamPaginate(dir) {
  975. event.currentTarget.blur();
  976. let url = dir == 'next' ? this.autospamPagination.next : this.autospamPagination.prev;
  977. this.fetchAutospam(url);
  978. },
  979. viewSpamReport(report) {
  980. this.viewingSpamReportLoading = false;
  981. this.viewingSpamReport = report;
  982. this.showSpamReportModal = true;
  983. window.history.pushState(null, null, '/i/admin/reports?tab=autospam&id=' + report.id);
  984. setTimeout(() => {
  985. pixelfed.readmore()
  986. }, 1000)
  987. },
  988. getSpamActionLabel(action) {
  989. switch(action) {
  990. case 'mark-all-read':
  991. return 'Are you sure you want to mark all spam reports by this account as read?';
  992. break;
  993. case 'mark-all-not-spam':
  994. return 'Are you sure you want to mark all spam reports by this account as not spam?';
  995. break;
  996. case 'delete-profile':
  997. return 'Are you sure you want to delete this profile?';
  998. break;
  999. }
  1000. },
  1001. handleSpamAction(action) {
  1002. event.currentTarget.blur();
  1003. this.viewingSpamReportLoading = true;
  1004. if(action !== 'mark-not-spam' && action !== 'mark-read' && !window.confirm(this.getSpamActionLabel(action))) {
  1005. this.viewingSpamReportLoading = false;
  1006. return;
  1007. }
  1008. this.loaded = false;
  1009. axios.post('/i/admin/api/reports/spam/handle', {
  1010. id: this.viewingSpamReport.id,
  1011. action: action,
  1012. })
  1013. .catch(err => {
  1014. swal('Error', err.response.data.error, 'error');
  1015. })
  1016. .finally(() => {
  1017. this.viewingSpamReportLoading = true;
  1018. this.viewingSpamReport = false;
  1019. this.showSpamReportModal = false;
  1020. setTimeout(() => {
  1021. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  1022. }, 500);
  1023. })
  1024. },
  1025. fetchReport(id) {
  1026. axios.get('/i/admin/api/reports/get/' + id)
  1027. .then(res => {
  1028. this.tabIndex = 0;
  1029. this.viewReport(res.data.data);
  1030. })
  1031. .catch(err => {
  1032. this.fetchStats();
  1033. window.history.pushState(null, null, '/i/admin/reports');
  1034. })
  1035. },
  1036. fetchSpamReport(id) {
  1037. axios.get('/i/admin/api/reports/spam/get/' + id)
  1038. .then(res => {
  1039. this.tabIndex = 2;
  1040. this.viewSpamReport(res.data.data);
  1041. })
  1042. .catch(err => {
  1043. this.fetchStats();
  1044. window.history.pushState(null, null, '/i/admin/reports');
  1045. })
  1046. }
  1047. }
  1048. }
  1049. </script>