1
0

AdminReports.vue 82 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686
  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. :class="['nav-link d-flex align-items-center', { active: tabIndex == 4}]"
  143. href="#"
  144. @click.prevent="toggleTab(4)">
  145. <span>Moderated Profiles</span>
  146. </a>
  147. </li>
  148. </ul>
  149. </div>
  150. </div>
  151. <div v-if="[0, 1].includes(this.tabIndex)" class="table-responsive rounded">
  152. <table v-if="reports && reports.length" class="table table-dark">
  153. <thead class="thead-dark">
  154. <tr>
  155. <th scope="col">ID</th>
  156. <th scope="col">Report</th>
  157. <th scope="col">Reported Account</th>
  158. <th scope="col">Reported By</th>
  159. <th scope="col">Created</th>
  160. <th scope="col">View Report</th>
  161. </tr>
  162. </thead>
  163. <tbody>
  164. <tr v-for="(report, idx) in reports">
  165. <td class="font-weight-bold text-monospace text-muted align-middle">
  166. <a href="#" @click.prevent="viewReport(report)">
  167. {{ report.id }}
  168. </a>
  169. </td>
  170. <td class="align-middle">
  171. <p class="text-capitalize font-weight-bold mb-0" v-html="reportLabel(report)"></p>
  172. </td>
  173. <td class="align-middle">
  174. <a v-if="report.reported && report.reported.id" :href="`/i/web/profile/${report.reported.id}`" target="_blank" class="text-white">
  175. <div class="d-flex align-items-center" style="gap:0.61rem;">
  176. <img
  177. :src="report.reported.avatar"
  178. width="30"
  179. height="30"
  180. style="object-fit: cover;border-radius:30px;"
  181. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  182. <div class="d-flex flex-column">
  183. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reported.username}}</p>
  184. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  185. <span>{{report.reported.followers_count}} Followers</span>
  186. <span>·</span>
  187. <span>Joined {{ timeAgo(report.reported.created_at) }}</span>
  188. </div>
  189. </div>
  190. </div>
  191. </a>
  192. </td>
  193. <td class="align-middle">
  194. <a
  195. v-if="report && report.reporter && report.reporter.id"
  196. :href="`/i/web/profile/${report.reporter.id}`"
  197. target="_blank"
  198. class="text-white">
  199. <div class="d-flex align-items-center" style="gap:0.61rem;">
  200. <img
  201. :src="report.reporter.avatar"
  202. width="30"
  203. height="30"
  204. style="object-fit: cover;border-radius:30px;"
  205. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  206. <div class="d-flex flex-column">
  207. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reporter.username}}</p>
  208. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  209. <span>{{report.reporter.followers_count}} Followers</span>
  210. <span>·</span>
  211. <span>Joined {{ timeAgo(report.reporter.created_at) }}</span>
  212. </div>
  213. </div>
  214. </div>
  215. </a>
  216. </td>
  217. <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
  218. <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="viewReport(report)">View</a></td>
  219. </tr>
  220. </tbody>
  221. </table>
  222. <div v-else>
  223. <div class="card card-body p-5">
  224. <div class="d-flex justify-content-between align-items-center flex-column">
  225. <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
  226. <p class="lead">{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}</p>
  227. </div>
  228. </div>
  229. </div>
  230. </div>
  231. <div v-if="[0, 1].includes(this.tabIndex) && reports.length && (pagination.prev || pagination.next)" class="d-flex align-items-center justify-content-center">
  232. <button
  233. class="btn btn-primary rounded-pill"
  234. :disabled="!pagination.prev"
  235. @click="paginate('prev')">
  236. Prev
  237. </button>
  238. <button
  239. class="btn btn-primary rounded-pill"
  240. :disabled="!pagination.next"
  241. @click="paginate('next')">
  242. Next
  243. </button>
  244. </div>
  245. <div v-if="this.tabIndex === 2" class="table-responsive rounded">
  246. <template v-if="autospamLoaded">
  247. <table v-if="autospam && autospam.length" class="table table-dark">
  248. <thead class="thead-dark">
  249. <tr>
  250. <th scope="col">ID</th>
  251. <th scope="col">Report</th>
  252. <th scope="col">Reported Account</th>
  253. <th scope="col">Created</th>
  254. <th scope="col">View Report</th>
  255. </tr>
  256. </thead>
  257. <tbody>
  258. <tr v-for="(report, idx) in autospam">
  259. <td class="font-weight-bold text-monospace text-muted align-middle">
  260. <a href="#" @click.prevent="viewSpamReport(report)">
  261. {{ report.id }}
  262. </a>
  263. </td>
  264. <td class="align-middle">
  265. <p class="text-capitalize font-weight-bold mb-0">Spam Post</p>
  266. </td>
  267. <td class="align-middle">
  268. <a v-if="report.status && report.status.account" :href="`/i/web/profile/${report.status.account.id}`" target="_blank" class="text-white">
  269. <div class="d-flex align-items-center" style="gap:0.61rem;">
  270. <img
  271. :src="report.status.account.avatar"
  272. width="30"
  273. height="30"
  274. style="object-fit: cover;border-radius:30px;"
  275. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  276. <div class="d-flex flex-column">
  277. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.status.account.username}}</p>
  278. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  279. <span>{{report.status.account.followers_count}} Followers</span>
  280. <span>·</span>
  281. <span>Joined {{ timeAgo(report.status.account.created_at) }}</span>
  282. </div>
  283. </div>
  284. </div>
  285. </a>
  286. </td>
  287. <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
  288. <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="viewSpamReport(report)">View</a></td>
  289. </tr>
  290. </tbody>
  291. </table>
  292. <div v-else>
  293. <div class="card card-body p-5">
  294. <div class="d-flex justify-content-between align-items-center flex-column">
  295. <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
  296. <p class="lead">No Spam Reports Found!</p>
  297. </div>
  298. </div>
  299. </div>
  300. </template>
  301. <div v-else class="d-flex align-items-center justify-content-center" style="min-height: 300px;">
  302. <b-spinner />
  303. </div>
  304. </div>
  305. <div v-if="this.tabIndex === 2 && autospamLoaded && autospam && autospam.length" class="d-flex align-items-center justify-content-center">
  306. <button
  307. class="btn btn-primary rounded-pill"
  308. :disabled="!autospamPagination.prev"
  309. @click="autospamPaginate('prev')">
  310. Prev
  311. </button>
  312. <button
  313. class="btn btn-primary rounded-pill"
  314. :disabled="!autospamPagination.next"
  315. @click="autospamPaginate('next')">
  316. Next
  317. </button>
  318. </div>
  319. <div v-if="this.tabIndex === 3" class="table-responsive rounded">
  320. <table v-if="reports && reports.length" class="table table-dark">
  321. <thead class="thead-dark">
  322. <tr>
  323. <th scope="col">ID</th>
  324. <th scope="col">Instance</th>
  325. <th scope="col">Reported Account</th>
  326. <th scope="col">Comment</th>
  327. <th scope="col">Created</th>
  328. <th scope="col">View Report</th>
  329. </tr>
  330. </thead>
  331. <tbody>
  332. <tr
  333. v-for="(report, idx) in reports"
  334. :key="`remote-reports-${report.id}-${idx}`">
  335. <td class="font-weight-bold text-monospace text-muted align-middle">
  336. <a href="#" @click.prevent="showRemoteReport(report)">
  337. {{ report.id }}
  338. </a>
  339. </td>
  340. <td class="align-middle">
  341. <p class="font-weight-bold mb-0">{{ report.instance }}</p>
  342. </td>
  343. <td class="align-middle">
  344. <a v-if="report.reported && report.reported.id" :href="`/i/web/profile/${report.reported.id}`" target="_blank" class="text-white">
  345. <div class="d-flex align-items-center" style="gap:0.61rem;">
  346. <img
  347. :src="report.reported.avatar"
  348. width="30"
  349. height="30"
  350. style="object-fit: cover;border-radius:30px;"
  351. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  352. <div class="d-flex flex-column">
  353. <p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reported.username}}</p>
  354. <div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
  355. <span>{{report.reported.followers_count}} Followers</span>
  356. <span>·</span>
  357. <span>Joined {{ timeAgo(report.reported.created_at) }}</span>
  358. </div>
  359. </div>
  360. </div>
  361. </a>
  362. </td>
  363. <td class="align-middle">
  364. <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>
  365. </td>
  366. <td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
  367. <td class="align-middle"><a href="#" class="btn btn-primary btn-sm" @click.prevent="showRemoteReport(report)">View</a></td>
  368. </tr>
  369. </tbody>
  370. </table>
  371. <div v-else>
  372. <div class="card card-body p-5">
  373. <div class="d-flex justify-content-between align-items-center flex-column">
  374. <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
  375. <p class="lead">{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}</p>
  376. </div>
  377. </div>
  378. </div>
  379. </div>
  380. <div v-if="this.tabIndex === 3 && remoteReportsLoaded && reports && reports.length" class="d-flex align-items-center justify-content-center">
  381. <button
  382. class="btn btn-primary rounded-pill"
  383. :disabled="!pagination.prev"
  384. @click="remoteReportPaginate('prev')">
  385. Prev
  386. </button>
  387. <button
  388. class="btn btn-primary rounded-pill"
  389. :disabled="!pagination.next"
  390. @click="remoteReportPaginate('next')">
  391. Next
  392. </button>
  393. </div>
  394. <div v-if="this.tabIndex === 4" class="table-responsive rounded">
  395. <div class="d-flex justify-content-between align-items-center mb-3">
  396. <form class="navbar-search navbar-search-dark form-inline mr-sm-3" @submit.prevent="handleModeratedProfileSearch">
  397. <div class="form-group mb-0">
  398. <div class="input-group input-group-alternative input-group-merge">
  399. <div class="input-group-prepend">
  400. <span class="input-group-text">
  401. <i class="fas fa-search"></i>
  402. </span>
  403. </div>
  404. <input
  405. type="text"
  406. name="username"
  407. placeholder="Search by username"
  408. class="form-control"
  409. v-model="moderatedProfilesSearchInput">
  410. </div>
  411. </div>
  412. </form>
  413. <div class="d-flex gap-1">
  414. <button type="button" class="btn btn-outline-primary fw-bold" @click="exportModeratedProfiles()">Export</button>
  415. <button type="button" class="btn btn-primary fw-bold" @click.prevent="addModeratedProfile()">Add Moderated Profile</button>
  416. </div>
  417. </div>
  418. <table v-if="moderatedProfiles && moderatedProfiles.length" class="table table-dark">
  419. <thead class="thead-dark">
  420. <tr>
  421. <th scope="col">ID</th>
  422. <th scope="col">Username</th>
  423. <th scope="col">Moderation</th>
  424. <th scope="col">Comment</th>
  425. <th scope="col">Created</th>
  426. </tr>
  427. </thead>
  428. <tbody>
  429. <tr
  430. v-for="(report, idx) in moderatedProfiles"
  431. :key="`remote-reports-${report.id}-${idx}`">
  432. <td class="font-weight-bold text-monospace text-muted align-middle">
  433. <button class="btn btn-primary btn-sm" @click.prevent="openModeratedProfileModal(report)">
  434. {{ report.id }}
  435. </button>
  436. </td>
  437. <td class="align-middle">
  438. <p v-if="report.profile.name" class="small mb-0 text-muted">
  439. {{ truncateText(report.profile.name, 40) }}
  440. </p>
  441. <p
  442. class="font-weight-bold mb-0"
  443. data-toggle="tooltip"
  444. data-placement="bottom"
  445. :title="report.profile.username">
  446. {{ truncateText(report.profile.username, 40) }}
  447. </p>
  448. </td>
  449. <td class="align-middle">
  450. <p class="mb-0" v-html="getModerationLabels(report)"></p>
  451. </td>
  452. <td class="align-middle">
  453. <p class="small mb-0 text-wrap" style="max-width: 200px;word-break: break-word;">{{ truncateText(report.note, 140) }}</p>
  454. </td>
  455. <td class="font-weight-bold align-middle">
  456. <span
  457. data-toggle="tooltip"
  458. data-placement="bottom"
  459. :title="report.created_at">
  460. {{ timeAgo(report.created_at) }}
  461. </span>
  462. </td>
  463. </tr>
  464. </tbody>
  465. </table>
  466. <div v-else>
  467. <div class="card card-body p-5">
  468. <div class="d-flex justify-content-between align-items-center flex-column">
  469. <template v-if="moderatedProfilesSearchInput">
  470. <p class="mt-3 mb-0"><i class="far fa-times fa-5x text-danger"></i></p>
  471. <p class="lead">No results found!</p>
  472. <button class="btn btn-primary" @click.prevent="clearModeratedProfileSearch()">Go back</button>
  473. </template>
  474. <template v-else>
  475. <p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
  476. <p class="lead">No active moderation accounts found!</p>
  477. </template>
  478. </div>
  479. </div>
  480. </div>
  481. <div v-if="moderatedProfiles && moderatedProfiles.length && (moderatedProfilesPagination.prev || moderatedProfilesPagination.next)" class="mt-3 d-flex align-items-center justify-content-center">
  482. <button
  483. class="btn btn-primary rounded-pill"
  484. :disabled="!moderatedProfilesPagination.prev"
  485. @click="paginateModeratedAccounts('prev')">
  486. Prev
  487. </button>
  488. <button
  489. class="btn btn-primary rounded-pill"
  490. :disabled="!moderatedProfilesPagination.next"
  491. @click="paginateModeratedAccounts('next')">
  492. Next
  493. </button>
  494. </div>
  495. </div>
  496. </div>
  497. </div>
  498. <b-modal v-model="showReportModal" :title="tabIndex === 0 ? 'View Report' : 'Viewing Closed Report'" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
  499. <div v-if="viewingReportLoading" class="d-flex align-items-center justify-content-center">
  500. <b-spinner />
  501. </div>
  502. <template v-else>
  503. <div v-if="viewingReport" class="list-group">
  504. <div class="list-group-item d-flex align-items-center justify-content-between">
  505. <div class="text-muted small">Type</div>
  506. <div class="font-weight-bold text-capitalize" v-html="reportLabel(viewingReport)"></div>
  507. </div>
  508. <div v-if="viewingReport.admin_seen_at" class="list-group-item d-flex align-items-center justify-content-between">
  509. <div class="text-muted small">Report Closed</div>
  510. <div class="font-weight-bold text-capitalize">{{ formatDate(viewingReport.admin_seen_at) }}</div>
  511. </div>
  512. <div v-if="viewingReport.reporter_message" class="list-group-item d-flex flex-column" style="gap:10px;">
  513. <div class="text-muted small">Message</div>
  514. <p class="mb-0 read-more" style="font-size: 12px;overflow-y: hidden;">{{ viewingReport.reporter_message }}</p>
  515. </div>
  516. </div>
  517. <div class="list-group list-group-horizontal mt-3">
  518. <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;">
  519. <div class="text-muted small font-weight-bold mt-n1">Reported Account</div>
  520. <a v-if="viewingReport.reported && viewingReport.reported.id" :href="`/i/web/profile/${viewingReport.reported.id}`" target="_blank" class="text-primary">
  521. <div class="d-flex align-items-center" style="gap:0.61rem;">
  522. <img
  523. :src="viewingReport.reported.avatar"
  524. width="30"
  525. height="30"
  526. style="object-fit: cover;border-radius:30px;"
  527. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  528. <div class="d-flex flex-column">
  529. <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>
  530. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  531. <span>{{viewingReport.reported.followers_count}} Followers</span>
  532. <span>·</span>
  533. <span>Joined {{ timeAgo(viewingReport.reported.created_at) }}</span>
  534. </div>
  535. </div>
  536. </div>
  537. </a>
  538. </div>
  539. <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;">
  540. <div class="text-muted small font-weight-bold mt-n1">Reporter Account</div>
  541. <a v-if="viewingReport.reporter && viewingReport.reporter?.id" :href="`/i/web/profile/${viewingReport.reporter?.id}`" target="_blank" class="text-primary">
  542. <div class="d-flex align-items-center" style="gap:0.61rem;">
  543. <img
  544. :src="viewingReport.reporter.avatar"
  545. width="30"
  546. height="30"
  547. style="object-fit: cover;border-radius:30px;"
  548. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  549. <div class="d-flex flex-column">
  550. <p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;">@{{viewingReport.reporter.acct}}</p>
  551. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  552. <span>{{viewingReport.reporter.followers_count}} Followers</span>
  553. <span>·</span>
  554. <span>Joined {{ timeAgo(viewingReport.reporter.created_at) }}</span>
  555. </div>
  556. </div>
  557. </div>
  558. </a>
  559. </div>
  560. </div>
  561. <div v-if="viewingReport && viewingReport.object_type === 'App\\Status' && viewingReport.status" class="list-group mt-3">
  562. <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;">
  563. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  564. <div>Reported Post</div>
  565. <a class="font-weight-bold" :href="viewingReport.status.url" target="_blank">View</a>
  566. </div>
  567. <img
  568. v-if="viewingReport.status.media_attachments[0].type === 'image'"
  569. :src="viewingReport.status.media_attachments[0].url"
  570. height="140"
  571. class="rounded"
  572. style="object-fit: cover;"
  573. onerror="this.src='/storage/no-preview.png';this.error=null;" />
  574. <video
  575. v-else-if="viewingReport.status.media_attachments[0].type === 'video'"
  576. height="140"
  577. controls
  578. :src="viewingReport.status.media_attachments[0].url"
  579. onerror="this.src='/storage/no-preview.png';this.onerror=null;"
  580. ></video>
  581. </div>
  582. <div v-if="viewingReport && viewingReport.status" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
  583. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  584. <div>Reported Post Caption</div>
  585. <a class="font-weight-bold" :href="viewingReport.status.url" target="_blank">View</a>
  586. </div>
  587. <p class="mb-0 read-more" style="font-size:12px;overflow-y: hidden">{{ viewingReport.status.content_text }}</p>
  588. </div>
  589. </div>
  590. <div v-else-if="viewingReport && viewingReport.object_type === 'App\\Story' && viewingReport.story" class="list-group mt-3">
  591. <div v-if="viewingReport && viewingReport.story" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
  592. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  593. <div>Reported Story</div>
  594. <a class="font-weight-bold" :href="viewingReport.story.url" target="_blank">View</a>
  595. </div>
  596. <img
  597. v-if="viewingReport.story.type === 'photo'"
  598. :src="viewingReport.story.media_src"
  599. height="140"
  600. class="rounded"
  601. style="object-fit: cover;"
  602. onerror="this.src='/storage/no-preview.png';this.error=null;" />
  603. <video
  604. v-else-if="viewingReport.story.type === 'video'"
  605. height="140"
  606. controls
  607. :src="viewingReport.story.media_src"
  608. onerror="this.src='/storage/no-preview.png';this.onerror=null;"
  609. ></video>
  610. </div>
  611. </div>
  612. <div v-if="viewingReport && viewingReport.admin_seen_at === null" class="mt-4">
  613. <div v-if="viewingReport && viewingReport.object_type === 'App\\Profile'">
  614. <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('profile', 'ignore')">Ignore Report</button>
  615. <hr v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin" class="mt-3 mb-1">
  616. <div
  617. v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin"
  618. class="d-flex flex-row mt-2"
  619. style="gap:0.3rem;">
  620. <button
  621. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
  622. @click="handleAction('profile', 'nsfw')">
  623. Mark all Posts NSFW
  624. </button>
  625. <button
  626. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
  627. @click="handleAction('profile', 'unlist')">
  628. Unlist all Posts
  629. </button>
  630. </div>
  631. <button
  632. v-if="viewingReport.reported && viewingReport.reported.id && !viewingReport.reported.is_admin"
  633. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-2"
  634. @click="handleAction('profile', 'delete')">
  635. Delete Profile
  636. </button>
  637. </div>
  638. <div v-else-if="viewingReport && viewingReport.object_type === 'App\\Status'">
  639. <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('post', 'ignore')">Ignore Report</button>
  640. <hr v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin" class="mt-3 mb-1">
  641. <div
  642. v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin"
  643. class="d-flex flex-row mt-2"
  644. style="gap:0.3rem;">
  645. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'nsfw')">Mark Post NSFW</button>
  646. <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>
  647. <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>
  648. </div>
  649. <div
  650. v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin"
  651. class="d-flex flex-row mt-2"
  652. style="gap:0.3rem;">
  653. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'nsfw')">Make all NSFW</button>
  654. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'unlist')">Make all Unlisted</button>
  655. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'private')">Make all Private</button>
  656. </div>
  657. <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
  658. <hr class="my-2">
  659. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  660. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('post', 'delete')">Delete Post</button>
  661. <button class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0" @click="handleAction('profile', 'delete')">Delete Account</button>
  662. </div>
  663. </div>
  664. </div>
  665. <div v-else-if="viewingReport && viewingReport.object_type === 'App\\Story'">
  666. <button class="btn btn-dark btn-block rounded-pill" @click="handleAction('story', 'ignore')">Ignore Report</button>
  667. <hr v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin" class="mt-3 mb-1">
  668. <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
  669. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  670. <button class="btn btn-danger btn-block rounded-pill mt-0" @click="handleAction('story', 'delete')">Delete Story</button>
  671. <button class="btn btn-outline-danger btn-block rounded-pill mt-0" @click="handleAction('story', 'delete-all')">Delete All Stories</button>
  672. </div>
  673. </div>
  674. <div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
  675. <hr class="my-2">
  676. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  677. <button class="btn btn-outline-danger btn-sm btn-block rounded-pill mt-0" @click="handleAction('profile', 'delete')">Delete Account</button>
  678. </div>
  679. </div>
  680. </div>
  681. </div>
  682. </template>
  683. </b-modal>
  684. <b-modal v-model="showSpamReportModal" title="Potential Spam Post Detected" :ok-only="true" ok-title="Close" ok-variant="outline-primary">
  685. <div v-if="viewingSpamReportLoading" class="d-flex align-items-center justify-content-center">
  686. <b-spinner />
  687. </div>
  688. <template v-else>
  689. <div class="list-group list-group-horizontal mt-3">
  690. <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;">
  691. <div class="text-muted small font-weight-bold mt-n1">Reported Account</div>
  692. <a v-if="viewingSpamReport.status.account && viewingSpamReport.status.account.id" :href="`/i/web/profile/${viewingSpamReport.status.account.id}`" target="_blank" class="text-primary">
  693. <div class="d-flex align-items-center" style="gap:0.61rem;">
  694. <img
  695. :src="viewingSpamReport.status.account.avatar"
  696. width="30"
  697. height="30"
  698. style="object-fit: cover;border-radius:30px;"
  699. onerror="this.src='/storage/avatars/default.png';this.error=null;">
  700. <div class="d-flex flex-column">
  701. <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>
  702. <div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
  703. <span>{{viewingSpamReport.status.account.followers_count}} Followers</span>
  704. <span>·</span>
  705. <span>Joined {{ timeAgo(viewingSpamReport.status.account.created_at) }}</span>
  706. </div>
  707. </div>
  708. </div>
  709. </a>
  710. </div>
  711. </div>
  712. <div v-if="viewingSpamReport && viewingSpamReport.status" class="list-group mt-3">
  713. <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;">
  714. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  715. <div>Reported Post</div>
  716. <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
  717. </div>
  718. <img
  719. v-if="viewingSpamReport.status.media_attachments[0].type === 'image'"
  720. :src="viewingSpamReport.status.media_attachments[0].url"
  721. height="140"
  722. class="rounded"
  723. style="object-fit: cover;"
  724. onerror="this.src='/storage/no-preview.png';this.error=null;" />
  725. <video
  726. v-else-if="viewingSpamReport.status.media_attachments[0].type === 'video'"
  727. height="140"
  728. controls
  729. :src="viewingSpamReport.status.media_attachments[0].url"
  730. onerror="this.src='/storage/no-preview.png';this.onerror=null;"
  731. ></video>
  732. </div>
  733. <div
  734. v-if="viewingSpamReport &&
  735. viewingSpamReport.status &&
  736. viewingSpamReport.status.content_text &&
  737. viewingSpamReport.status.content_text.length"
  738. class="list-group-item d-flex flex-column flex-grow-1"
  739. style="gap:0.4rem;">
  740. <div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
  741. <div>Reported Post Caption</div>
  742. <a class="font-weight-bold" :href="viewingSpamReport.status.url" target="_blank">View</a>
  743. </div>
  744. <p class="mb-0 read-more" style="font-size:12px;overflow-y: hidden">{{ viewingSpamReport.status.content_text }}</p>
  745. </div>
  746. </div>
  747. <div class="mt-4">
  748. <div>
  749. <button
  750. type="button"
  751. class="btn btn-dark btn-block rounded-pill"
  752. @click="handleSpamAction('mark-read')">
  753. Mark as Read
  754. </button>
  755. <button
  756. type="button"
  757. class="btn btn-danger btn-block rounded-pill"
  758. @click="handleSpamAction('mark-not-spam')">
  759. Mark As Not Spam
  760. </button>
  761. <hr class="mt-3 mb-1">
  762. <div
  763. class="d-flex flex-row mt-2"
  764. style="gap:0.3rem;">
  765. <button
  766. type="button"
  767. class="btn btn-dark btn-block btn-sm rounded-pill mt-0"
  768. @click="handleSpamAction('mark-all-read')">
  769. Mark All As Read
  770. </button>
  771. <button
  772. type="button"
  773. class="btn btn-dark btn-block btn-sm rounded-pill mt-0"
  774. @click="handleSpamAction('mark-all-not-spam')">
  775. Mark All As Not Spam
  776. </button>
  777. </div>
  778. <div>
  779. <hr class="my-2">
  780. <div class="d-flex flex-row mt-2" style="gap:0.3rem;">
  781. <button
  782. type="button"
  783. class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
  784. @click="handleSpamAction('delete-profile')">
  785. Delete Account
  786. </button>
  787. </div>
  788. </div>
  789. </div>
  790. </div>
  791. </template>
  792. </b-modal>
  793. <template v-if="showRemoteReportModal">
  794. <admin-report-modal
  795. :open="showRemoteReportModal"
  796. :model="remoteReportModalModel"
  797. v-on:close="handleCloseRemoteReportModal()"
  798. v-on:refresh="refreshRemoteReports()" />
  799. </template>
  800. <div
  801. class="modal fade"
  802. id="moderatedProfileView"
  803. tabindex="-1"
  804. role="dialog"
  805. aria-labelledby="moderatedProfileViewLabel"
  806. aria-hidden="true"
  807. data-backdrop="static"
  808. ref="moderatedProfileModal">
  809. <div class="modal-dialog modal-dialog-centered" role="document">
  810. <div v-if="modModalData" class="modal-content">
  811. <div class="modal-header">
  812. <div class="w-100 d-flex justify-content-between align-items-center">
  813. <div class="flex-grow-1">
  814. <i class="far fa-shield-alt"></i>
  815. </div>
  816. <h5 class="mb-0 lead mt-0 font-weight-bold">Moderated Profile</h5>
  817. <div class="flex-grow-1">
  818. <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="closeModeratedProfileModal()">
  819. <span aria-hidden="true">&times;</span>
  820. </button>
  821. </div>
  822. </div>
  823. </div>
  824. <div class="modal-body">
  825. <div class="card mb-0">
  826. <div class="card-body bg-lighter text-dark p-3 font-weight-bold d-flex align-items-center justify-content-center flex-column">
  827. <p v-if="modModalData?.profile?.name" class="mb-0 small text-muted">{{ modModalData?.profile?.name }}</p>
  828. <p class="mb-0 font-weight-bold">
  829. {{ modModalData?.profile?.username }}
  830. </p>
  831. </div>
  832. </div>
  833. <p v-if="modModalData?.profile?.remote_url" class="small text-muted text-right mb-1">
  834. <a :href="modModalData?.profile?.remote_url" rel="noreferrer" target="_blank">
  835. View remote profile
  836. </a>
  837. </p>
  838. <div class="list-group mpl-form">
  839. <div class="list-group-item d-flex justify-content-between align-items-center">
  840. <div class="mp-form-label">
  841. <div class="d-flex flex-column">
  842. <p class="mb-0 font-weight-bold">
  843. Banned
  844. </p>
  845. <p class="mb-0 small text-muted">
  846. Ban any activities from this account.
  847. </p>
  848. </div>
  849. </div>
  850. <div class="custom-control custom-checkbox">
  851. <input type="checkbox" class="custom-control-input" id="mp-form-is_banned" v-model="modModalModel.is_banned">
  852. <label class="custom-control-label" for="mp-form-is_banned"></label>
  853. </div>
  854. </div>
  855. <div class="list-group-item d-none justify-content-between align-items-center">
  856. <div class="mp-form-label">
  857. <div class="d-flex flex-column">
  858. <p class="mb-0 font-weight-bold">
  859. No Autolink
  860. </p>
  861. <p class="mb-0 small text-muted">
  862. Disable hashtag, mention and url autolinking from this account.
  863. </p>
  864. </div>
  865. </div>
  866. <div class="custom-control custom-checkbox">
  867. <input type="checkbox" class="custom-control-input" id="mp-form-is_noautolink" v-model="modModalModel.is_noautolink">
  868. <label class="custom-control-label" for="mp-form-is_noautolink"></label>
  869. </div>
  870. </div>
  871. <div class="list-group-item d-none justify-content-between align-items-center">
  872. <div class="mp-form-label">
  873. <div class="d-flex flex-column">
  874. <p class="mb-0 font-weight-bold">
  875. No DMs
  876. </p>
  877. <p class="mb-0 small text-muted">
  878. Ignore DMs from this account.
  879. </p>
  880. </div>
  881. </div>
  882. <div class="custom-control custom-checkbox">
  883. <input type="checkbox" class="custom-control-input" id="mp-form-is_nodms" v-model="modModalModel.is_nodms">
  884. <label class="custom-control-label" for="mp-form-is_nodms"></label>
  885. </div>
  886. </div>
  887. <div class="list-group-item d-none justify-content-between align-items-center">
  888. <div class="mp-form-label">
  889. <div class="d-flex flex-column">
  890. <p class="mb-0 font-weight-bold">
  891. No Trending
  892. </p>
  893. <p class="mb-0 small text-muted">
  894. Prevent posts from this account from appearing in trending lists or feeds.
  895. </p>
  896. </div>
  897. </div>
  898. <div class="custom-control custom-checkbox">
  899. <input type="checkbox" class="custom-control-input" id="mp-form-is_notrending" v-model="modModalModel.is_notrending">
  900. <label class="custom-control-label" for="mp-form-is_notrending"></label>
  901. </div>
  902. </div>
  903. <div class="list-group-item d-none justify-content-between align-items-center">
  904. <div class="mp-form-label">
  905. <div class="d-flex flex-column">
  906. <p class="mb-0 font-weight-bold">
  907. Mark NSFW
  908. </p>
  909. <p class="mb-0 small text-muted">
  910. Mark all posts as sensitive, and apply CWs to future posts.
  911. </p>
  912. </div>
  913. </div>
  914. <div class="custom-control custom-checkbox">
  915. <input type="checkbox" class="custom-control-input" id="mp-form-is_nsfw" v-model="modModalModel.is_nsfw">
  916. <label class="custom-control-label" for="mp-form-is_nsfw"></label>
  917. </div>
  918. </div>
  919. <div class="list-group-item d-none justify-content-between align-items-center">
  920. <div class="mp-form-label">
  921. <div class="d-flex flex-column">
  922. <p class="mb-0 font-weight-bold">
  923. Mark Unlisted
  924. </p>
  925. <p class="mb-0 small text-muted">
  926. Mark all future posts as unlisted, hidden from global/tag feeds.
  927. </p>
  928. </div>
  929. </div>
  930. <div class="custom-control custom-checkbox">
  931. <input type="checkbox" class="custom-control-input" id="mp-form-is_unlisted" v-model="modModalModel.is_unlisted">
  932. <label class="custom-control-label" for="mp-form-is_unlisted"></label>
  933. </div>
  934. </div>
  935. </div>
  936. <div class="py-3">
  937. <label class="small text-muted">Account Notes (only visible to admins)</label>
  938. <textarea class="form-control" v-model="modModalData.note" placeholder="Add an optional note" maxlength="500"></textarea>
  939. </div>
  940. </div>
  941. <div class="modal-footer d-flex justify-content-between align-items-center">
  942. <button type="button" class="btn btn-link text-dark" data-dismiss="modal" @click="closeModeratedProfileModal()">Close</button>
  943. <div class="d-flex flex-grow-1 align-items-center gap-1">
  944. <button type="button" class="btn btn-danger" @click.prevent="handleModProfileModalDelete()">Delete</button>
  945. <button type="button" class="btn btn-primary btn-block" @click.prevent="handleModProfileModalUpdate()">Save</button>
  946. </div>
  947. </div>
  948. </div>
  949. </div>
  950. </div>
  951. </div>
  952. </template>
  953. <script type="text/javascript">
  954. import AdminRemoteReportModal from "./partial/AdminRemoteReportModal.vue";
  955. export default {
  956. components: {
  957. "admin-report-modal": AdminRemoteReportModal
  958. },
  959. data() {
  960. return {
  961. loaded: false,
  962. stats: {
  963. total: 0,
  964. open: 0,
  965. closed: 0,
  966. autospam: 0,
  967. autospam_open: 0,
  968. remote_open: 0,
  969. },
  970. tabIndex: 0,
  971. reports: [],
  972. pagination: {},
  973. showReportModal: false,
  974. viewingReport: undefined,
  975. viewingReportLoading: false,
  976. autospam: [],
  977. autospamPagination: {},
  978. autospamLoaded: false,
  979. showSpamReportModal: false,
  980. viewingSpamReport: undefined,
  981. viewingSpamReportLoading: false,
  982. remoteReportsLoaded: false,
  983. showRemoteReportModal: undefined,
  984. remoteReportModalModel: {},
  985. moderatedProfiles: [],
  986. moderatedProfilesPagination: {},
  987. moderatedProfilesSearchInput: undefined,
  988. modModalData: undefined,
  989. modModalModel: {},
  990. }
  991. },
  992. mounted() {
  993. let u = new URLSearchParams(window.location.search);
  994. if(u.has('tab') && u.get('tab') === 'moderated-profiles' && u.has('action') && u.has('id') && u.get('action') === 'view') {
  995. this.tabIndex = 4;
  996. this.fetchModeratedAccounts();
  997. this.fetchModeratedProfile(u.get('id'));
  998. } else if(u.has('tab') && u.get('tab') === 'autospam' && !u.has('id')) {
  999. this.tabIndex = 2;
  1000. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  1001. } else if(u.has('tab') && u.get('tab') === 'closed') {
  1002. this.tabIndex = 1;
  1003. this.fetchStats('/i/admin/api/reports/all?filter=closed')
  1004. } else if(u.has('tab') && u.get('tab') === 'closed') {
  1005. this.tabIndex = 3;
  1006. this.fetchStats('/i/admin/api/reports/all?filter=remote')
  1007. } else if(u.has('tab') && u.get('tab') === 'moderated-profiles') {
  1008. this.tabIndex = 4;
  1009. this.fetchModeratedAccounts();
  1010. } else if(u.has('tab') && u.has('id') && u.get('tab') === 'autospam') {
  1011. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  1012. this.fetchSpamReport(u.get('id'));
  1013. } else if(u.has('tab') && u.has('id') && u.get('tab') === 'report') {
  1014. this.fetchStats();
  1015. this.fetchReport(u.get('id'));
  1016. } else {
  1017. window.history.pushState(null, null, '/i/admin/reports');
  1018. this.fetchStats();
  1019. }
  1020. this.$root.$on('bv::modal::hide', (bvEvent, modalId) => {
  1021. window.history.pushState(null, null, '/i/admin/reports');
  1022. })
  1023. },
  1024. methods: {
  1025. toggleTab(idx) {
  1026. switch(idx) {
  1027. case 0:
  1028. this.fetchStats('/i/admin/api/reports/all');
  1029. window.history.pushState(null, null, '/i/admin/reports');
  1030. break;
  1031. case 1:
  1032. this.fetchStats('/i/admin/api/reports/all?filter=closed')
  1033. window.history.pushState(null, null, '/i/admin/reports?tab=closed');
  1034. break;
  1035. case 2:
  1036. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  1037. window.history.pushState(null, null, '/i/admin/reports?tab=autospam');
  1038. break;
  1039. case 3:
  1040. this.fetchRemoteReports();
  1041. window.history.pushState(null, null, '/i/admin/reports?tab=remote');
  1042. break;
  1043. case 4:
  1044. this.fetchModeratedAccounts();
  1045. window.history.pushState(null, null, '/i/admin/reports?tab=moderated-profiles');
  1046. break;
  1047. }
  1048. this.tabIndex = idx;
  1049. },
  1050. prettyCount(str) {
  1051. if(str) {
  1052. return str.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
  1053. }
  1054. return str;
  1055. },
  1056. timeAgo(str) {
  1057. if(!str) {
  1058. return str;
  1059. }
  1060. return App.util.format.timeAgo(str);
  1061. },
  1062. formatDate(str) {
  1063. let date = new Date(str);
  1064. return new Intl.DateTimeFormat('default', {
  1065. month: 'long',
  1066. day: 'numeric',
  1067. year: 'numeric',
  1068. hour: 'numeric',
  1069. minute: 'numeric'
  1070. }).format(date);
  1071. },
  1072. reportLabel(report) {
  1073. switch(report.object_type) {
  1074. case 'App\\Profile':
  1075. return `${report.type} Profile`;
  1076. break;
  1077. case 'App\\Status':
  1078. return `${report.type} Post`;
  1079. break;
  1080. case 'App\\Story':
  1081. return `${report.type} Story`;
  1082. break;
  1083. }
  1084. },
  1085. fetchStats(fetchReportsUrl = '/i/admin/api/reports/all', fetchSpamUrl = null) {
  1086. axios.get('/i/admin/api/reports/stats')
  1087. .then(res => {
  1088. this.stats = res.data;
  1089. })
  1090. .finally(() => {
  1091. if(fetchReportsUrl) {
  1092. this.fetchReports(fetchReportsUrl);
  1093. } else if(fetchSpamUrl) {
  1094. this.fetchAutospam(fetchSpamUrl);
  1095. }
  1096. $('[data-toggle="tooltip"]').tooltip()
  1097. });
  1098. },
  1099. fetchModeratedAccounts(apiUrl = '/i/admin/api/reports/moderated-profiles') {
  1100. axios.get(apiUrl)
  1101. .then(res => {
  1102. this.moderatedProfiles = res.data.data;
  1103. this.moderatedProfilesPagination = {
  1104. prev: res.data.links.prev,
  1105. next: res.data.links.next
  1106. };
  1107. })
  1108. .finally(() => {
  1109. this.loaded = true;
  1110. $('[data-toggle="tooltip"]').tooltip()
  1111. })
  1112. },
  1113. paginateModeratedAccounts(dir) {
  1114. event.currentTarget.blur();
  1115. let url = dir == 'next' ? this.moderatedProfilesPagination.next : this.moderatedProfilesPagination.prev;
  1116. this.fetchModeratedAccounts(url);
  1117. },
  1118. fetchReports(url = '/i/admin/api/reports/all') {
  1119. axios.get(url)
  1120. .then(res => {
  1121. this.reports = res.data.data;
  1122. this.pagination = {
  1123. next: res.data.links.next,
  1124. prev: res.data.links.prev
  1125. };
  1126. })
  1127. .finally(() => {
  1128. this.loaded = true;
  1129. });
  1130. },
  1131. fetchRemoteReports(url = '/i/admin/api/reports/remote') {
  1132. axios.get(url)
  1133. .then(res => {
  1134. this.reports = res.data.data;
  1135. this.pagination = {
  1136. next: res.data.links.next,
  1137. prev: res.data.links.prev
  1138. };
  1139. })
  1140. .finally(() => {
  1141. this.loaded = true;
  1142. this.remoteReportsLoaded = true;
  1143. });
  1144. },
  1145. remoteReportPaginate(dir) {
  1146. event.currentTarget.blur();
  1147. let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
  1148. this.fetchRemoteReports(url);
  1149. },
  1150. handleCloseRemoteReportModal() {
  1151. this.showRemoteReportModal = false;
  1152. },
  1153. showRemoteReport(report) {
  1154. this.remoteReportModalModel = report;
  1155. this.showRemoteReportModal = true;
  1156. },
  1157. refreshRemoteReports() {
  1158. this.fetchStats('');
  1159. this.$nextTick(() => {
  1160. this.toggleTab(3);
  1161. })
  1162. },
  1163. paginate(dir) {
  1164. event.currentTarget.blur();
  1165. let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
  1166. this.fetchReports(url);
  1167. },
  1168. viewReport(report) {
  1169. this.viewingReportLoading = false;
  1170. this.viewingReport = report;
  1171. this.showReportModal = true;
  1172. window.history.pushState(null, null, '/i/admin/reports?tab=report&id=' + report.id);
  1173. setTimeout(() => {
  1174. pixelfed.readmore()
  1175. }, 1000)
  1176. },
  1177. handleAction(type, action) {
  1178. event.currentTarget.blur();
  1179. this.viewingReportLoading = true;
  1180. if(action !== 'ignore' && !window.confirm(this.getActionLabel(type, action))) {
  1181. this.viewingReportLoading = false;
  1182. return;
  1183. }
  1184. this.loaded = false;
  1185. axios.post('/i/admin/api/reports/handle', {
  1186. id: this.viewingReport.id,
  1187. object_id: this.viewingReport.object_id,
  1188. object_type: this.viewingReport.object_type,
  1189. action: action,
  1190. action_type: type
  1191. })
  1192. .catch(err => {
  1193. swal('Error', err.response.data.error, 'error');
  1194. })
  1195. .finally(() => {
  1196. this.viewingReportLoading = true;
  1197. this.viewingReport = false;
  1198. this.showReportModal = false;
  1199. setTimeout(() => {
  1200. this.fetchStats();
  1201. }, 1000);
  1202. })
  1203. },
  1204. getActionLabel(type, action) {
  1205. if(type === 'profile') {
  1206. switch(action) {
  1207. case 'ignore':
  1208. return 'Are you sure you want to ignore this profile report?';
  1209. break;
  1210. case 'nsfw':
  1211. return 'Are you sure you want to mark this profile as NSFW?';
  1212. break;
  1213. case 'unlist':
  1214. return 'Are you sure you want to mark all posts by this profile as unlisted?';
  1215. break;
  1216. case 'private':
  1217. return 'Are you sure you want to mark all posts by this profile as private?';
  1218. break;
  1219. case 'delete':
  1220. return 'Are you sure you want to delete this profile?';
  1221. break;
  1222. }
  1223. } else if(type === 'post') {
  1224. switch(action) {
  1225. case 'ignore':
  1226. return 'Are you sure you want to ignore this post report?';
  1227. break;
  1228. case 'nsfw':
  1229. return 'Are you sure you want to mark this post as NSFW?';
  1230. break;
  1231. case 'unlist':
  1232. return 'Are you sure you want to mark this post as unlisted?';
  1233. break;
  1234. case 'private':
  1235. return 'Are you sure you want to mark this post as private?';
  1236. break;
  1237. case 'delete':
  1238. return 'Are you sure you want to delete this post?';
  1239. break;
  1240. }
  1241. } else if(type === 'story') {
  1242. switch(action) {
  1243. case 'ignore':
  1244. return 'Are you sure you want to ignore this story report?';
  1245. break;
  1246. case 'delete':
  1247. return 'Are you sure you want to delete this story?';
  1248. break;
  1249. case 'delete-all':
  1250. return 'Are you sure you want to delete all stories by this account?';
  1251. break;
  1252. }
  1253. }
  1254. },
  1255. fetchAutospam(url = '/i/admin/api/reports/spam/all') {
  1256. axios.get(url)
  1257. .then(res => {
  1258. this.autospam = res.data.data;
  1259. this.autospamPagination = {
  1260. next: res.data.links.next,
  1261. prev: res.data.links.prev
  1262. }
  1263. })
  1264. .finally(() => {
  1265. this.autospamLoaded = true;
  1266. this.loaded = true;
  1267. })
  1268. },
  1269. autospamPaginate(dir) {
  1270. event.currentTarget.blur();
  1271. let url = dir == 'next' ? this.autospamPagination.next : this.autospamPagination.prev;
  1272. this.fetchAutospam(url);
  1273. },
  1274. viewSpamReport(report) {
  1275. this.viewingSpamReportLoading = false;
  1276. this.viewingSpamReport = report;
  1277. this.showSpamReportModal = true;
  1278. window.history.pushState(null, null, '/i/admin/reports?tab=autospam&id=' + report.id);
  1279. setTimeout(() => {
  1280. pixelfed.readmore()
  1281. }, 1000)
  1282. },
  1283. getSpamActionLabel(action) {
  1284. switch(action) {
  1285. case 'mark-all-read':
  1286. return 'Are you sure you want to mark all spam reports by this account as read?';
  1287. break;
  1288. case 'mark-all-not-spam':
  1289. return 'Are you sure you want to mark all spam reports by this account as not spam?';
  1290. break;
  1291. case 'delete-profile':
  1292. return 'Are you sure you want to delete this profile?';
  1293. break;
  1294. }
  1295. },
  1296. handleSpamAction(action) {
  1297. event.currentTarget.blur();
  1298. this.viewingSpamReportLoading = true;
  1299. if(action !== 'mark-not-spam' && action !== 'mark-read' && !window.confirm(this.getSpamActionLabel(action))) {
  1300. this.viewingSpamReportLoading = false;
  1301. return;
  1302. }
  1303. this.loaded = false;
  1304. axios.post('/i/admin/api/reports/spam/handle', {
  1305. id: this.viewingSpamReport.id,
  1306. action: action,
  1307. })
  1308. .catch(err => {
  1309. swal('Error', err.response.data.error, 'error');
  1310. })
  1311. .finally(() => {
  1312. this.viewingSpamReportLoading = true;
  1313. this.viewingSpamReport = false;
  1314. this.showSpamReportModal = false;
  1315. setTimeout(() => {
  1316. this.fetchStats(null, '/i/admin/api/reports/spam/all');
  1317. }, 500);
  1318. })
  1319. },
  1320. fetchReport(id) {
  1321. axios.get('/i/admin/api/reports/get/' + id)
  1322. .then(res => {
  1323. this.tabIndex = 0;
  1324. this.viewReport(res.data.data);
  1325. })
  1326. .catch(err => {
  1327. this.fetchStats();
  1328. window.history.pushState(null, null, '/i/admin/reports');
  1329. })
  1330. },
  1331. fetchSpamReport(id) {
  1332. axios.get('/i/admin/api/reports/spam/get/' + id)
  1333. .then(res => {
  1334. this.tabIndex = 2;
  1335. this.viewSpamReport(res.data.data);
  1336. })
  1337. .catch(err => {
  1338. this.fetchStats();
  1339. window.history.pushState(null, null, '/i/admin/reports');
  1340. })
  1341. },
  1342. truncateText(text, maxLength, appendEllipsis = true) {
  1343. if(!text || !text.length) {
  1344. return
  1345. }
  1346. if (text.length <= maxLength) {
  1347. return text;
  1348. }
  1349. const truncated = text.slice(0, maxLength).trim();
  1350. return appendEllipsis ? truncated + '...' : truncated;
  1351. },
  1352. getModerationLabels(acct) {
  1353. if(acct.is_banned) {
  1354. return `<span class="badge badge-danger">Banned</span>`
  1355. }
  1356. let labels = [];
  1357. if(acct.is_banned) labels.push('Banned')
  1358. if(acct.is_noautolink) labels.push('No Autolink')
  1359. if(acct.is_nodms) labels.push('No DMS')
  1360. if(acct.is_notrending) labels.push('No Trending')
  1361. if(acct.is_nsfw) labels.push('NSFW')
  1362. if(acct.is_unlisted) labels.push('Unlisted')
  1363. return labels.map((item, index) => {
  1364. const colorClass = item === 'Banned' ? 'danger' : 'primary';
  1365. return `<span class="badge badge-${colorClass}">${item}</span>`;
  1366. }).join(' ');
  1367. },
  1368. handleModeratedProfileSearch(event) {
  1369. event.currentTarget.blur()
  1370. let url = `/i/admin/api/reports/moderated-profiles?search=${this.moderatedProfilesSearchInput}`
  1371. this.fetchModeratedAccounts(url)
  1372. },
  1373. clearModeratedProfileSearch() {
  1374. this.moderatedProfilesSearchInput = undefined;
  1375. this.fetchModeratedAccounts();
  1376. },
  1377. openModeratedProfileModal(report) {
  1378. this.modModalData = report;
  1379. this.modModalModel = {
  1380. is_banned: report.is_banned,
  1381. is_noautolink: report.is_noautolink,
  1382. is_nodms: report.is_nodms,
  1383. is_notrending: report.is_notrending,
  1384. is_nsfw: report.is_nsfw,
  1385. is_unlisted: report.is_unlisted,
  1386. }
  1387. $(this.$refs.moderatedProfileModal).modal('show');
  1388. window.history.pushState(null, null, `/i/admin/reports?tab=moderated-profiles&action=view&id=${report.id}`)
  1389. },
  1390. handleModProfileModalUpdate() {
  1391. axios.post(
  1392. '/i/admin/api/reports/moderated-profiles/update',
  1393. {...this.modModalData, ...this.modModalModel}
  1394. ).then(res => {
  1395. window.history.pushState(null, null, `/i/admin/reports?tab=moderated-profiles`)
  1396. window.location.reload();
  1397. }).catch(error => {
  1398. let errorMessage = 'An error occurred';
  1399. if (error.response) {
  1400. errorMessage = `Error ${error.response.status}: ${error.response.data.error || error.response.data.message || error.response.statusText}`;
  1401. } else if (error.request) {
  1402. errorMessage = 'No response received from server';
  1403. } else {
  1404. errorMessage = error.message;
  1405. }
  1406. swal('Error', errorMessage, 'error')
  1407. }).finally(() => {
  1408. $(this.$refs.moderatedProfileModal).modal('hide');
  1409. })
  1410. },
  1411. handleModProfileModalDelete() {
  1412. swal({
  1413. title: 'Confirm Delete',
  1414. text: 'Are you sure you want to delete this moderated profile ruleset?',
  1415. buttons: {
  1416. cancel: "Cancel",
  1417. danger: {
  1418. text: "Delete",
  1419. value: 'delete',
  1420. }
  1421. }
  1422. }).then((val) => {
  1423. if(val === 'delete') {
  1424. axios.post('/i/admin/api/reports/moderated-profiles/delete', { id: this.modModalData.id})
  1425. .then(res => {
  1426. window.history.pushState(null, null, '/i/admin/reports?tab=moderated-profiles');
  1427. window.location.reload();
  1428. })
  1429. }
  1430. $(this.$refs.moderatedProfileModal).modal('hide');
  1431. swal.close()
  1432. })
  1433. },
  1434. fetchModeratedProfile(id) {
  1435. axios.get(`/i/admin/api/reports/moderated-profiles/show?id=${id}`)
  1436. .then(res => {
  1437. this.modModalData = res.data.data;
  1438. let report = res.data.data;
  1439. this.modModalModel = {
  1440. is_banned: report.is_banned,
  1441. is_noautolink: report.is_noautolink,
  1442. is_nodms: report.is_nodms,
  1443. is_notrending: report.is_notrending,
  1444. is_nsfw: report.is_nsfw,
  1445. is_unlisted: report.is_unlisted,
  1446. }
  1447. $(this.$refs.moderatedProfileModal).modal('show');
  1448. }).catch(err => {
  1449. window.history.pushState(null, null, '/i/admin/reports?tab=moderated-profiles');
  1450. swal('Error', 'Invalid moderated profile id!', 'error');
  1451. })
  1452. },
  1453. addModeratedProfile() {
  1454. swal({
  1455. text: 'Enter profile URL (ie: https://mastodon.social/@Mastodon)',
  1456. content: "input",
  1457. button: {
  1458. text: "Add",
  1459. closeModal: false,
  1460. },
  1461. }).then(val => {
  1462. if (!val) throw null;
  1463. if(val.startsWith('@')) {
  1464. swal('Error', 'Invalid URL, webfinger is not supported yet.', 'error');
  1465. throw null;
  1466. }
  1467. if(!val.startsWith('http')) {
  1468. swal('Error', 'Invalid URL', 'error');
  1469. throw null;
  1470. }
  1471. if(val.indexOf('.') === -1) {
  1472. swal('Error', 'Invalid URL', 'error');
  1473. throw null;
  1474. }
  1475. let params = {
  1476. url: val
  1477. }
  1478. return axios.post('/i/admin/api/reports/moderated-profiles/create', params);
  1479. }).then(json => {
  1480. if(json && json.data && json.data?.id) {
  1481. window.location.href = `/i/admin/reports?tab=moderated-profiles&action=view&id=${json.data?.id}`
  1482. return;
  1483. }
  1484. swal.stopLoading();
  1485. swal.close();
  1486. }).catch(err => {
  1487. if (err) {
  1488. if(err?.response?.data?.error) {
  1489. swal("Error", err?.response?.data?.error, "error");
  1490. } else {
  1491. swal("Error", "Something went wrong!", "error");
  1492. }
  1493. } else {
  1494. swal.stopLoading();
  1495. swal.close();
  1496. }
  1497. });
  1498. },
  1499. closeModeratedProfileModal() {
  1500. window.history.pushState(null, null, '/i/admin/reports?tab=moderated-profiles');
  1501. },
  1502. exportModeratedProfiles() {
  1503. axios.get('/i/admin/api/reports/moderated-profiles/export', {
  1504. responseType: "blob"
  1505. })
  1506. .then(res => {
  1507. let host = new URL(window.location.href)
  1508. let date = new Date();
  1509. let dateStamp = `${date.getMonth()}-${date.getDate()}-${date.getFullYear()}-${Date.now()}`;
  1510. let filename = host.host + '-moderated-profiles-' + dateStamp + '.json';
  1511. let el = document.createElement('a');
  1512. el.setAttribute('download', filename)
  1513. const href = URL.createObjectURL(res.data);
  1514. el.href = href;
  1515. el.setAttribute('target', '_blank');
  1516. el.click();
  1517. swal(
  1518. 'Success!',
  1519. 'You have successfully exported the moderated profile backup.',
  1520. 'success'
  1521. )
  1522. })
  1523. }
  1524. }
  1525. }
  1526. </script>
  1527. <style lang="scss" scoped>
  1528. .mpl-form {
  1529. p {
  1530. line-height: 1;
  1531. &:first-child {
  1532. font-size: 14px;
  1533. line-height: 1.6;
  1534. }
  1535. }
  1536. }
  1537. </style>