mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 14:31:26 +00:00
Merge pull request #4962 from pixelfed/staging
Add Remote Reports to Admin Dashboard Reports page
This commit is contained in:
commit
189e87f28a
12 changed files with 987 additions and 5 deletions
|
@ -5,6 +5,8 @@
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
|
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
|
||||||
|
- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
|
||||||
|
- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
|
||||||
|
|
||||||
### Updates
|
### Updates
|
||||||
|
|
||||||
|
@ -18,6 +20,10 @@
|
||||||
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
|
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
|
||||||
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
|
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
|
||||||
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
|
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
|
||||||
|
- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
|
||||||
|
- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
|
||||||
|
- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
|
||||||
|
- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
|
||||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||||
|
|
||||||
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
|
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\AccountInterstitial;
|
use App\AccountInterstitial;
|
||||||
use App\Http\Resources\AdminReport;
|
use App\Http\Resources\AdminReport;
|
||||||
|
use App\Http\Resources\AdminRemoteReport;
|
||||||
use App\Http\Resources\AdminSpamReport;
|
use App\Http\Resources\AdminSpamReport;
|
||||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||||
|
@ -13,6 +14,7 @@ use App\Jobs\StoryPipeline\StoryDelete;
|
||||||
use App\Notification;
|
use App\Notification;
|
||||||
use App\Profile;
|
use App\Profile;
|
||||||
use App\Report;
|
use App\Report;
|
||||||
|
use App\Models\RemoteReport;
|
||||||
use App\Services\AccountService;
|
use App\Services\AccountService;
|
||||||
use App\Services\ModLogService;
|
use App\Services\ModLogService;
|
||||||
use App\Services\NetworkTimelineService;
|
use App\Services\NetworkTimelineService;
|
||||||
|
@ -23,6 +25,7 @@ use App\Status;
|
||||||
use App\Story;
|
use App\Story;
|
||||||
use App\User;
|
use App\User;
|
||||||
use Cache;
|
use Cache;
|
||||||
|
use Storage;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
@ -640,6 +643,7 @@ trait AdminReportController
|
||||||
'autospam' => AccountInterstitial::whereType('post.autospam')->count(),
|
'autospam' => AccountInterstitial::whereType('post.autospam')->count(),
|
||||||
'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(),
|
'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(),
|
||||||
'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(),
|
'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(),
|
||||||
|
'remote_open' => RemoteReport::whereNull('action_taken_at')->count(),
|
||||||
'email_verification_requests' => Redis::scard('email:manual'),
|
'email_verification_requests' => Redis::scard('email:manual'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -665,6 +669,24 @@ trait AdminReportController
|
||||||
return $reports;
|
return $reports;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function reportsApiRemote(Request $request)
|
||||||
|
{
|
||||||
|
$filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
|
||||||
|
|
||||||
|
$reports = AdminRemoteReport::collection(
|
||||||
|
RemoteReport::orderBy('id', 'desc')
|
||||||
|
->when($filter, function ($q, $filter) {
|
||||||
|
return $filter == 'open' ?
|
||||||
|
$q->whereNull('action_taken_at') :
|
||||||
|
$q->whereNotNull('action_taken_at');
|
||||||
|
})
|
||||||
|
->cursorPaginate(6)
|
||||||
|
->withQueryString()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $reports;
|
||||||
|
}
|
||||||
|
|
||||||
public function reportsApiGet(Request $request, $id)
|
public function reportsApiGet(Request $request, $id)
|
||||||
{
|
{
|
||||||
$report = Report::findOrFail($id);
|
$report = Report::findOrFail($id);
|
||||||
|
@ -1327,4 +1349,173 @@ trait AdminReportController
|
||||||
|
|
||||||
return new AdminSpamReport($report);
|
return new AdminSpamReport($report);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function reportsApiRemoteHandle(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'id' => 'required|exists:remote_reports,id',
|
||||||
|
'action' => 'required|in:mark-read,cw-posts,unlist-posts,delete-posts,private-posts,mark-all-read-by-domain,mark-all-read-by-username,cw-all-posts,private-all-posts,unlist-all-posts'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$report = RemoteReport::findOrFail($request->input('id'));
|
||||||
|
$user = User::whereProfileId($report->account_id)->first();
|
||||||
|
$ogPublicStatuses = [];
|
||||||
|
$ogUnlistedStatuses = [];
|
||||||
|
$ogNonCwStatuses = [];
|
||||||
|
|
||||||
|
switch ($request->input('action')) {
|
||||||
|
case 'mark-read':
|
||||||
|
$report->action_taken_at = now();
|
||||||
|
$report->save();
|
||||||
|
break;
|
||||||
|
case 'mark-all-read-by-domain':
|
||||||
|
RemoteReport::whereInstanceId($report->instance_id)->update(['action_taken_at' => now()]);
|
||||||
|
break;
|
||||||
|
case 'cw-posts':
|
||||||
|
$statuses = Status::find($report->status_ids);
|
||||||
|
foreach($statuses as $status) {
|
||||||
|
if($report->account_id != $status->profile_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!$status->is_nsfw) {
|
||||||
|
$ogNonCwStatuses[] = $status->id;
|
||||||
|
}
|
||||||
|
$status->is_nsfw = true;
|
||||||
|
$status->saveQuietly();
|
||||||
|
StatusService::del($status->id);
|
||||||
|
}
|
||||||
|
$report->action_taken_at = now();
|
||||||
|
$report->save();
|
||||||
|
break;
|
||||||
|
case 'cw-all-posts':
|
||||||
|
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
|
||||||
|
if($status->is_nsfw || $status->reblog_of_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!$status->is_nsfw) {
|
||||||
|
$ogNonCwStatuses[] = $status->id;
|
||||||
|
}
|
||||||
|
$status->is_nsfw = true;
|
||||||
|
$status->saveQuietly();
|
||||||
|
StatusService::del($status->id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'unlist-posts':
|
||||||
|
$statuses = Status::find($report->status_ids);
|
||||||
|
foreach($statuses as $status) {
|
||||||
|
if($report->account_id != $status->profile_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if($status->scope === 'public') {
|
||||||
|
$ogPublicStatuses[] = $status->id;
|
||||||
|
$status->scope = 'unlisted';
|
||||||
|
$status->visibility = 'unlisted';
|
||||||
|
$status->saveQuietly();
|
||||||
|
StatusService::del($status->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$report->action_taken_at = now();
|
||||||
|
$report->save();
|
||||||
|
break;
|
||||||
|
case 'unlist-all-posts':
|
||||||
|
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
|
||||||
|
if($status->visibility !== 'public' || $status->reblog_of_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ogPublicStatuses[] = $status->id;
|
||||||
|
$status->visibility = 'unlisted';
|
||||||
|
$status->scope = 'unlisted';
|
||||||
|
$status->saveQuietly();
|
||||||
|
StatusService::del($status->id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'private-posts':
|
||||||
|
$statuses = Status::find($report->status_ids);
|
||||||
|
foreach($statuses as $status) {
|
||||||
|
if($report->account_id != $status->profile_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
||||||
|
if($status->scope === 'public') {
|
||||||
|
$ogPublicStatuses[] = $status->id;
|
||||||
|
}
|
||||||
|
$status->scope = 'private';
|
||||||
|
$status->visibility = 'private';
|
||||||
|
$status->saveQuietly();
|
||||||
|
StatusService::del($status->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$report->action_taken_at = now();
|
||||||
|
$report->save();
|
||||||
|
break;
|
||||||
|
case 'private-all-posts':
|
||||||
|
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
|
||||||
|
if(!in_array($status->visibility, ['public', 'unlisted']) || $status->reblog_of_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if($status->visibility === 'public') {
|
||||||
|
$ogPublicStatuses[] = $status->id;
|
||||||
|
} else if($status->visibility === 'unlisted') {
|
||||||
|
$ogUnlistedStatuses[] = $status->id;
|
||||||
|
}
|
||||||
|
$status->visibility = 'private';
|
||||||
|
$status->scope = 'private';
|
||||||
|
$status->saveQuietly();
|
||||||
|
StatusService::del($status->id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'delete-posts':
|
||||||
|
$statuses = Status::find($report->status_ids);
|
||||||
|
foreach($statuses as $status) {
|
||||||
|
if($report->account_id != $status->profile_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StatusDelete::dispatch($status);
|
||||||
|
}
|
||||||
|
$report->action_taken_at = now();
|
||||||
|
$report->save();
|
||||||
|
break;
|
||||||
|
case 'mark-all-read-by-username':
|
||||||
|
RemoteReport::whereNull('action_taken_at')->whereAccountId($report->account_id)->update(['action_taken_at' => now()]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
abort(404);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($ogPublicStatuses && count($ogPublicStatuses)) {
|
||||||
|
Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-public-statuses.json', json_encode($ogPublicStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($ogNonCwStatuses && count($ogNonCwStatuses)) {
|
||||||
|
Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-noncw-statuses.json', json_encode($ogNonCwStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($ogUnlistedStatuses && count($ogUnlistedStatuses)) {
|
||||||
|
Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-unlisted-statuses.json', json_encode($ogUnlistedStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
ModLogService::boot()
|
||||||
|
->user(request()->user())
|
||||||
|
->objectUid($user ? $user->id : null)
|
||||||
|
->objectId($report->id)
|
||||||
|
->objectType('App\Report::class')
|
||||||
|
->action('admin.report.moderate')
|
||||||
|
->metadata([
|
||||||
|
'action' => $request->input('action'),
|
||||||
|
'duration_active' => now()->parse($report->created_at)->diffForHumans()
|
||||||
|
])
|
||||||
|
->accessLevel('admin')
|
||||||
|
->save();
|
||||||
|
|
||||||
|
if($report->status_ids) {
|
||||||
|
foreach($report->status_ids as $sid) {
|
||||||
|
RemoteReport::whereNull('action_taken_at')
|
||||||
|
->whereJsonContains('status_ids', [$sid])
|
||||||
|
->update(['action_taken_at' => now()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [200];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,6 +104,10 @@ class AdminCuratedRegisterController extends Controller
|
||||||
|
|
||||||
foreach ($activities as $activity) {
|
foreach ($activities as $activity) {
|
||||||
$idx++;
|
$idx++;
|
||||||
|
|
||||||
|
if ($activity->type === 'user_resend_email_confirmation') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ($activity->from_user) {
|
if ($activity->from_user) {
|
||||||
$userResponses->push($activity);
|
$userResponses->push($activity);
|
||||||
|
|
||||||
|
|
49
app/Http/Resources/AdminRemoteReport.php
Normal file
49
app/Http/Resources/AdminRemoteReport.php
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use App\Instance;
|
||||||
|
use App\Services\AccountService;
|
||||||
|
use App\Services\StatusService;
|
||||||
|
|
||||||
|
class AdminRemoteReport extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
$instance = parse_url($this->uri, PHP_URL_HOST);
|
||||||
|
$statuses = [];
|
||||||
|
if($this->status_ids && count($this->status_ids)) {
|
||||||
|
foreach($this->status_ids as $sid) {
|
||||||
|
$s = StatusService::get($sid, false);
|
||||||
|
if($s && $s['in_reply_to_id'] != null) {
|
||||||
|
$parent = StatusService::get($s['in_reply_to_id'], false);
|
||||||
|
if($parent) {
|
||||||
|
$s['parent'] = $parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($s) {
|
||||||
|
$statuses[] = $s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$res = [
|
||||||
|
'id' => $this->id,
|
||||||
|
'instance' => $instance,
|
||||||
|
'reported' => AccountService::get($this->account_id, true),
|
||||||
|
'status_ids' => $this->status_ids,
|
||||||
|
'statuses' => $statuses,
|
||||||
|
'message' => $this->comment,
|
||||||
|
'report_meta' => $this->report_meta,
|
||||||
|
'created_at' => optional($this->created_at)->format('c'),
|
||||||
|
'action_taken_at' => optional($this->action_taken_at)->format('c'),
|
||||||
|
];
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1243,7 +1243,14 @@ class Inbox
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$content = isset($this->payload['content']) ? Purify::clean($this->payload['content']) : null;
|
$content = null;
|
||||||
|
if(isset($this->payload['content'])) {
|
||||||
|
if(strlen($this->payload['content']) > 5000) {
|
||||||
|
$content = Purify::clean(substr($this->payload['content'], 0, 5000) . ' ... (truncated message due to exceeding max length)');
|
||||||
|
} else {
|
||||||
|
$content = Purify::clean($this->payload['content']);
|
||||||
|
}
|
||||||
|
}
|
||||||
$object = $this->payload['object'];
|
$object = $this->payload['object'];
|
||||||
|
|
||||||
if(empty($object) || (!is_array($object) && !is_string($object))) {
|
if(empty($object) || (!is_array($object) && !is_string($object))) {
|
||||||
|
@ -1259,7 +1266,7 @@ class Inbox
|
||||||
|
|
||||||
foreach($object as $objectUrl) {
|
foreach($object as $objectUrl) {
|
||||||
if(!Helpers::validateLocalUrl($objectUrl)) {
|
if(!Helpers::validateLocalUrl($objectUrl)) {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(str_contains($objectUrl, '/users/')) {
|
if(str_contains($objectUrl, '/users/')) {
|
||||||
|
@ -1280,6 +1287,23 @@ class Inbox
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($objects->count()) {
|
||||||
|
$obc = $objects->count();
|
||||||
|
if($obc > 25) {
|
||||||
|
if($obc > 30) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
$objLimit = $objects->take(20);
|
||||||
|
$objects = collect($objLimit->all());
|
||||||
|
$obc = $objects->count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$count = Status::whereProfileId($accountId)->find($objects)->count();
|
||||||
|
if($obc !== $count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$instanceHost = parse_url($id, PHP_URL_HOST);
|
$instanceHost = parse_url($id, PHP_URL_HOST);
|
||||||
|
|
||||||
$instance = Instance::updateOrCreate([
|
$instance = Instance::updateOrCreate([
|
||||||
|
|
BIN
public/js/admin.js
vendored
BIN
public/js/admin.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -103,6 +103,21 @@
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
:class="['nav-link d-flex align-items-center', { active: tabIndex == 3}]"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="toggleTab(3)">
|
||||||
|
|
||||||
|
<span>Remote Reports</span>
|
||||||
|
<span
|
||||||
|
v-if="stats.remote_open"
|
||||||
|
class="badge badge-sm badge-floating badge-danger border-white ml-2"
|
||||||
|
style="background-color: red;color:white;font-size:11px;">
|
||||||
|
{{prettyCount(stats.remote_open)}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="d-none d-md-block nav-item">
|
<li class="d-none d-md-block nav-item">
|
||||||
<a
|
<a
|
||||||
:class="['nav-link d-flex align-items-center', { active: tabIndex == 1}]"
|
:class="['nav-link d-flex align-items-center', { active: tabIndex == 1}]"
|
||||||
|
@ -191,7 +206,11 @@
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<a :href="`/i/web/profile/${report.reporter.id}`" target="_blank" class="text-white">
|
<a
|
||||||
|
v-if="report && report.reporter && report.reporter.id"
|
||||||
|
:href="`/i/web/profile/${report.reporter.id}`"
|
||||||
|
target="_blank"
|
||||||
|
class="text-white">
|
||||||
<div class="d-flex align-items-center" style="gap:0.61rem;">
|
<div class="d-flex align-items-center" style="gap:0.61rem;">
|
||||||
<img
|
<img
|
||||||
:src="report.reporter.avatar"
|
:src="report.reporter.avatar"
|
||||||
|
@ -320,6 +339,85 @@
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="this.tabIndex === 3" class="table-responsive rounded">
|
||||||
|
<table v-if="reports && reports.length" class="table table-dark">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">Instance</th>
|
||||||
|
<th scope="col">Reported Account</th>
|
||||||
|
<th scope="col">Comment</th>
|
||||||
|
<th scope="col">Created</th>
|
||||||
|
<th scope="col">View Report</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(report, idx) in reports"
|
||||||
|
:key="`remote-reports-${report.id}-${idx}`">
|
||||||
|
<td class="font-weight-bold text-monospace text-muted align-middle">
|
||||||
|
<a href="#" @click.prevent="showRemoteReport(report)">
|
||||||
|
{{ report.id }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<p class="font-weight-bold mb-0">{{ report.instance }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<a v-if="report.reported && report.reported.id" :href="`/i/web/profile/${report.reported.id}`" target="_blank" class="text-white">
|
||||||
|
<div class="d-flex align-items-center" style="gap:0.61rem;">
|
||||||
|
<img
|
||||||
|
:src="report.reported.avatar"
|
||||||
|
width="30"
|
||||||
|
height="30"
|
||||||
|
style="object-fit: cover;border-radius:30px;"
|
||||||
|
onerror="this.src='/storage/avatars/default.png';this.error=null;">
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reported.username}}</p>
|
||||||
|
<div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
|
||||||
|
<span>{{report.reported.followers_count}} Followers</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>Joined {{ timeAgo(report.reported.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<p class="small mb-0 text-wrap" style="max-width: 300px;word-break: break-all;">{{ report.message && report.message.length > 120 ? report.message.slice(0, 120) + '...' : report.message }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
|
||||||
|
<td class="align-middle"><a href="#" class="btn btn-primary btn-sm">View</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="card card-body p-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-column">
|
||||||
|
<p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
|
||||||
|
<p class="lead">{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="this.tabIndex === 3 && remoteReportsLoaded && reports && reports.length" class="d-flex align-items-center justify-content-center">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary rounded-pill"
|
||||||
|
:disabled="!pagination.prev"
|
||||||
|
@click="remoteReportPaginate('prev')">
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary rounded-pill"
|
||||||
|
:disabled="!pagination.next"
|
||||||
|
@click="remoteReportPaginate('next')">
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -372,7 +470,7 @@
|
||||||
<div v-if="viewingReport && viewingReport.reporter" class="list-group-item d-flex align-items-center justify-content-between flex-column flex-grow-1" style="gap:0.4rem;">
|
<div v-if="viewingReport && viewingReport.reporter" class="list-group-item d-flex align-items-center justify-content-between flex-column flex-grow-1" style="gap:0.4rem;">
|
||||||
<div class="text-muted small font-weight-bold mt-n1">Reporter Account</div>
|
<div class="text-muted small font-weight-bold mt-n1">Reporter Account</div>
|
||||||
|
|
||||||
<a v-if="viewingReport.reporter && viewingReport.reporter.id" :href="`/i/web/profile/${viewingReport.reporter.id}`" target="_blank" class="text-primary">
|
<a v-if="viewingReport.reporter && viewingReport.reporter?.id" :href="`/i/web/profile/${viewingReport.reporter?.id}`" target="_blank" class="text-primary">
|
||||||
<div class="d-flex align-items-center" style="gap:0.61rem;">
|
<div class="d-flex align-items-center" style="gap:0.61rem;">
|
||||||
<img
|
<img
|
||||||
:src="viewingReport.reporter.avatar"
|
:src="viewingReport.reporter.avatar"
|
||||||
|
@ -650,11 +748,25 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
|
||||||
|
<template v-if="showRemoteReportModal">
|
||||||
|
<admin-report-modal
|
||||||
|
:open="showRemoteReportModal"
|
||||||
|
:model="remoteReportModalModel"
|
||||||
|
v-on:close="handleCloseRemoteReportModal()"
|
||||||
|
v-on:refresh="refreshRemoteReports()" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
import AdminRemoteReportModal from "./partial/AdminRemoteReportModal.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
"admin-report-modal": AdminRemoteReportModal
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
@ -664,6 +776,7 @@
|
||||||
closed: 0,
|
closed: 0,
|
||||||
autospam: 0,
|
autospam: 0,
|
||||||
autospam_open: 0,
|
autospam_open: 0,
|
||||||
|
remote_open: 0,
|
||||||
},
|
},
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
reports: [],
|
reports: [],
|
||||||
|
@ -676,7 +789,10 @@
|
||||||
autospamLoaded: false,
|
autospamLoaded: false,
|
||||||
showSpamReportModal: false,
|
showSpamReportModal: false,
|
||||||
viewingSpamReport: undefined,
|
viewingSpamReport: undefined,
|
||||||
viewingSpamReportLoading: false
|
viewingSpamReportLoading: false,
|
||||||
|
remoteReportsLoaded: false,
|
||||||
|
showRemoteReportModal: undefined,
|
||||||
|
remoteReportModalModel: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -712,6 +828,10 @@
|
||||||
case 2:
|
case 2:
|
||||||
this.fetchStats(null, '/i/admin/api/reports/spam/all');
|
this.fetchStats(null, '/i/admin/api/reports/spam/all');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
this.fetchRemoteReports();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
window.history.pushState(null, null, '/i/admin/reports');
|
window.history.pushState(null, null, '/i/admin/reports');
|
||||||
this.tabIndex = idx;
|
this.tabIndex = idx;
|
||||||
|
@ -785,6 +905,43 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchRemoteReports(url = '/i/admin/api/reports/remote') {
|
||||||
|
axios.get(url)
|
||||||
|
.then(res => {
|
||||||
|
this.reports = res.data.data;
|
||||||
|
this.pagination = {
|
||||||
|
next: res.data.links.next,
|
||||||
|
prev: res.data.links.prev
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
this.remoteReportsLoaded = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
remoteReportPaginate(dir) {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
|
||||||
|
this.fetchRemoteReports(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCloseRemoteReportModal() {
|
||||||
|
this.showRemoteReportModal = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
showRemoteReport(report) {
|
||||||
|
this.remoteReportModalModel = report;
|
||||||
|
this.showRemoteReportModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshRemoteReports() {
|
||||||
|
this.fetchStats('');
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.toggleTab(3);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
paginate(dir) {
|
paginate(dir) {
|
||||||
event.currentTarget.blur();
|
event.currentTarget.blur();
|
||||||
let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
|
let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
|
||||||
|
|
139
resources/assets/components/admin/partial/AdminModalPost.vue
Normal file
139
resources/assets/components/admin/partial/AdminModalPost.vue
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div v-if="status.media_attachments && status.media_attachments.length" class="list-group-item" style="gap:1rem;overflow:hidden;">
|
||||||
|
<div class="text-center text-muted small font-weight-bold mb-3">Reported Post Media</div>
|
||||||
|
<div v-if="status.media_attachments && status.media_attachments.length" class="d-flex flex-grow-1" style="gap: 1rem;overflow-x:auto;">
|
||||||
|
<template
|
||||||
|
v-for="media in status.media_attachments">
|
||||||
|
<img
|
||||||
|
v-if="media.type === 'image'"
|
||||||
|
:src="media.url"
|
||||||
|
width="70"
|
||||||
|
height="70"
|
||||||
|
class="rounded"
|
||||||
|
style="object-fit: cover;"
|
||||||
|
@click="toggleLightbox"
|
||||||
|
onerror="this.src='/storage/no-preview.png';this.error=null;" />
|
||||||
|
|
||||||
|
<video
|
||||||
|
v-else-if="media.type === 'video'"
|
||||||
|
width="140"
|
||||||
|
height="90"
|
||||||
|
playsinline
|
||||||
|
@click.prevent="toggleVideoLightbox($event, media.url)"
|
||||||
|
class="rounded"
|
||||||
|
>
|
||||||
|
<source :src="media.url" :type="media.mime">
|
||||||
|
</video>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item d-flex flex-row flex-grow-1" style="gap:1rem;">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div v-if="status && status.in_reply_to_id && status.parent && status.parent.account" class="mb-3">
|
||||||
|
<template v-if="showInReplyTo">
|
||||||
|
<div class="mt-n1 text-center text-muted small font-weight-bold mb-1">Reply to</div>
|
||||||
|
<div class="media" style="gap: 1rem;">
|
||||||
|
<img
|
||||||
|
:src="status.parent.account.avatar"
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
class="rounded-lg"
|
||||||
|
onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="font-weight-bold mb-0" style="font-size: 11px;">
|
||||||
|
<a :href="`/i/web/profile/${status.parent.account.id}`" target="_blank">{{ status.parent.account.acct }}</a>
|
||||||
|
</p>
|
||||||
|
<admin-read-more :content="status.parent.content_text" />
|
||||||
|
<p class="mb-1">
|
||||||
|
<a :href="`/i/web/post/${status.parent.id}`" target="_blank" class="text-muted" style="font-size: 11px;">
|
||||||
|
<i class="far fa-link mr-1"></i> {{ formatDate(status.parent.created_at)}}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="my-1">
|
||||||
|
</template>
|
||||||
|
<a v-else class="btn btn-dark font-weight-bold btn-block btn-sm" href="#" @click.prevent="showInReplyTo = true">Show parent post</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="mt-n1 text-center text-muted small font-weight-bold mb-1">Reported Post</div>
|
||||||
|
<div class="media" style="gap: 1rem;">
|
||||||
|
<img
|
||||||
|
:src="status.account.avatar"
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
class="rounded-lg"
|
||||||
|
onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="font-weight-bold mb-0" style="font-size: 11px;">
|
||||||
|
<a :href="`/i/web/profile/${status.account.id}`" target="_blank">{{ status.account.acct }}</a>
|
||||||
|
</p>
|
||||||
|
<template v-if="status && status.content_text && status.content_text.length">
|
||||||
|
<admin-read-more :content="status.content_text" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<admin-read-more content="EMPTY CAPTION" class="font-weight-bold text-muted" />
|
||||||
|
</template>
|
||||||
|
<p class="mb-0">
|
||||||
|
<a :href="`/i/web/post/${status.id}`" target="_blank" class="text-muted" style="font-size: 11px;">
|
||||||
|
<i class="far fa-link mr-1"></i> {{ formatDate(status.created_at)}}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BigPicture from 'bigpicture';
|
||||||
|
import AdminReadMore from './AdminReadMore.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
status: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showInReplyTo: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
"admin-read-more": AdminReadMore
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleLightbox(e) {
|
||||||
|
BigPicture({
|
||||||
|
el: e.target
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleVideoLightbox($event, src) {
|
||||||
|
BigPicture({
|
||||||
|
el: event.target,
|
||||||
|
vidSrc: src
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(str) {
|
||||||
|
let date = new Date(str);
|
||||||
|
return new Intl.DateTimeFormat('default', {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric'
|
||||||
|
}).format(date);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
106
resources/assets/components/admin/partial/AdminReadMore.vue
Normal file
106
resources/assets/components/admin/partial/AdminReadMore.vue
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mb-0" :style="{ 'font-size':`${fontSize}px` }">{{ contentText }}</div>
|
||||||
|
<p class="mb-0"><a v-if="canStepExpand || (canExpand && !expanded)" class="font-weight-bold small" href="#" @click="expand()">Read more</a></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
content: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 140
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
type: String,
|
||||||
|
default: "13"
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
stepLimit: {
|
||||||
|
type: Number,
|
||||||
|
default: 140
|
||||||
|
},
|
||||||
|
initialLimit: {
|
||||||
|
type: Number,
|
||||||
|
default: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
contentText: {
|
||||||
|
get() {
|
||||||
|
if(this.step) {
|
||||||
|
const len = this.content.length;
|
||||||
|
const steps = len / this.stepLimit;
|
||||||
|
if(this.stepIndex == 1 || steps < this.stepIndex) {
|
||||||
|
this.canStepExpand = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.steppedTruncate();
|
||||||
|
}
|
||||||
|
if(this.content && this.content.length > this.maxLength) {
|
||||||
|
this.canExpand = true;
|
||||||
|
}
|
||||||
|
return this.expanded ? this.content : this.truncate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expanded: false,
|
||||||
|
canExpand: false,
|
||||||
|
canStepExpand: false,
|
||||||
|
stepIndex: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
expand() {
|
||||||
|
if(this.step) {
|
||||||
|
this.stepIndex++;
|
||||||
|
this.canStepExpand = true;
|
||||||
|
} else {
|
||||||
|
this.expanded = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
truncate() {
|
||||||
|
if(!this.content || !this.content.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.content && this.content.length < this.maxLength) {
|
||||||
|
return this.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.content.slice(0, this.maxLength) + '...';
|
||||||
|
},
|
||||||
|
|
||||||
|
steppedTruncate() {
|
||||||
|
if(!this.content || !this.content.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const len = this.content.length;
|
||||||
|
const steps = len / this.stepLimit;
|
||||||
|
const maxLen = this.stepLimit * this.stepIndex;
|
||||||
|
if(this.initialLimit != 10 && this.stepIndex === 1 && this.canStepExpand) {
|
||||||
|
this.canStepExpand = len > this.stepLimit;
|
||||||
|
return this.content.slice(0, this.initialLimit);
|
||||||
|
} else if(this.canStepExpand && this.stepIndex < steps) {
|
||||||
|
return this.content.slice(0, maxLen);
|
||||||
|
} else {
|
||||||
|
this.canStepExpand = false;
|
||||||
|
return this.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,304 @@
|
||||||
|
<template>
|
||||||
|
<b-modal
|
||||||
|
v-model="isOpen"
|
||||||
|
title="Remote Report"
|
||||||
|
:ok-only="true"
|
||||||
|
ok-title="Close"
|
||||||
|
:lazy="true"
|
||||||
|
:scrollable="true"
|
||||||
|
ok-variant="outline-primary"
|
||||||
|
v-on:hide="$emit('close')">
|
||||||
|
<div v-if="isLoading" class="d-flex align-items-center justify-content-center">
|
||||||
|
<b-spinner />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="list-group">
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div class="text-muted small font-weight-bold">Instance</div>
|
||||||
|
<div class="font-weight-bold">{{ model.instance }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="model.message && model.message.length" class="list-group-item d-flex justify-content-between align-items-center flex-column gap-1">
|
||||||
|
<div class="text-muted small font-weight-bold mb-2">Message</div>
|
||||||
|
<div class="text-wrap w-100" style="word-break:break-all;font-size:12.5px;">
|
||||||
|
<admin-read-more
|
||||||
|
:content="model.message"
|
||||||
|
font-size="11"
|
||||||
|
:step="true"
|
||||||
|
:initial-limit="100"
|
||||||
|
:stepLimit="1000" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-horizontal mt-3">
|
||||||
|
<div
|
||||||
|
v-if="model && model.reported"
|
||||||
|
class="list-group-item d-flex align-items-center justify-content-between flex-row flex-grow-1"
|
||||||
|
style="gap:0.4rem;">
|
||||||
|
<div class="text-muted small font-weight-bold">Reported Account</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end flex-grow-1">
|
||||||
|
<a v-if="model.reported && model.reported.id" :href="`/i/web/profile/${model.reported.id}`" target="_blank" class="text-primary">
|
||||||
|
<div class="d-flex align-items-center" style="gap:0.61rem;">
|
||||||
|
<img
|
||||||
|
:src="model.reported.avatar"
|
||||||
|
width="30"
|
||||||
|
height="30"
|
||||||
|
style="object-fit: cover;border-radius:30px;"
|
||||||
|
onerror="this.src='/storage/avatars/default.png';this.error=null;">
|
||||||
|
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;" :class="[ model.reported.is_admin ? 'text-danger': '']">@{{model.reported.acct}}</p>
|
||||||
|
<div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
|
||||||
|
<span>{{prettyCount(model.reported.followers_count)}} Followers</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>Joined {{ timeAgo(model.reported.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="list-group-item d-flex align-items-center justify-content-center flex-column flex-grow-1">
|
||||||
|
<p class="font-weight-bold mb-0">Reported Account Unavailable</p>
|
||||||
|
<p class="small mb-0">The reported account may have been deleted, or is otherwise not currently active. You can safely <strong>Close Report</strong> to mark this report as read.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="model && model.statuses && model.statuses.length" class="list-group mt-3">
|
||||||
|
<admin-modal-post
|
||||||
|
v-for="(status, idx) in model.statuses"
|
||||||
|
:key="`admin-modal-post-remote-post:${status.id}:${idx}`"
|
||||||
|
:status="status"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-dark btn-block rounded-pill"
|
||||||
|
@click="handleAction('mark-read')">
|
||||||
|
Close Report
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-dark btn-block text-center rounded-pill"
|
||||||
|
style="word-break: break-all;"
|
||||||
|
@click="handleAction('mark-all-read-by-domain')">
|
||||||
|
<span class="font-weight-light">Close all reports from</span> <strong>{{ model.instance}}</strong>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="model.reported"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-dark btn-block rounded-pill flex-grow-1"
|
||||||
|
@click="handleAction('mark-all-read-by-username')">
|
||||||
|
<span class="font-weight-light">Close all reports against</span> <strong>@{{ model.reported.username }}</strong>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-if="model && model.statuses && model.statuses.length && model.reported">
|
||||||
|
<hr class="mt-3 mb-1">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="d-flex flex-row mt-2"
|
||||||
|
style="gap:0.3rem;">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
|
||||||
|
@click="handleAction('cw-posts')">
|
||||||
|
Apply CW to Post(s)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
|
||||||
|
@click="handleAction('unlist-posts')">
|
||||||
|
Unlist Post(s)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row mt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
|
||||||
|
@click="handleAction('private-posts')">
|
||||||
|
Make Post(s) Private
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
|
||||||
|
@click="handleAction('delete-posts')">
|
||||||
|
Delete Post(s)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="model && model.statuses && !model.statuses.length && model.reported">
|
||||||
|
<hr class="mt-3 mb-1">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="d-flex flex-row mt-2"
|
||||||
|
style="gap:0.3rem;">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
|
||||||
|
@click="handleAction('cw-all-posts')">
|
||||||
|
Apply CW to all posts
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
|
||||||
|
@click="handleAction('unlist-all-posts')">
|
||||||
|
Unlist all account posts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="d-flex flex-row mt-2"
|
||||||
|
style="gap:0.3rem;">
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
|
||||||
|
@click="handleAction('private-all-posts')">
|
||||||
|
Make all posts private
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</b-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AdminModalPost from "./AdminModalPost.vue";
|
||||||
|
import AdminReadMore from "./AdminReadMore.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
open: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
"admin-modal-post": AdminModalPost,
|
||||||
|
"admin-read-more": AdminReadMore
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
open: {
|
||||||
|
handler() {
|
||||||
|
this.isOpen = this.open;
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
isOpen: false,
|
||||||
|
actions: [
|
||||||
|
'mark-read',
|
||||||
|
'cw-posts',
|
||||||
|
'unlist-posts',
|
||||||
|
'private-posts',
|
||||||
|
'delete-posts',
|
||||||
|
'mark-all-read-by-domain',
|
||||||
|
'mark-all-read-by-username',
|
||||||
|
'cw-all-posts',
|
||||||
|
'unlist-all-posts',
|
||||||
|
'private-all-posts',
|
||||||
|
],
|
||||||
|
actionMap: {
|
||||||
|
'cw-posts': 'apply content warnings to all post(s) in this report?',
|
||||||
|
'unlist-posts': 'unlist all post(s) in this report?',
|
||||||
|
'delete-posts': 'delete all post(s) in this report?',
|
||||||
|
'private-posts': 'make all post(s) in this report private/followers-only?',
|
||||||
|
'mark-all-read-by-domain': 'mark all reports by this instance as closed?',
|
||||||
|
'mark-all-read-by-username': 'mark all reports against this user as closed?',
|
||||||
|
'cw-all-posts': 'apply content warnings to all post(s) belonging to this account?',
|
||||||
|
'unlist-all-posts': 'make all post(s) belonging to this account as unlisted?',
|
||||||
|
'private-all-posts': 'make all post(s) belonging to this account as private?',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
prettyCount(str) {
|
||||||
|
if(str) {
|
||||||
|
return str.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
},
|
||||||
|
|
||||||
|
timeAgo(str) {
|
||||||
|
if(!str) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return App.util.format.timeAgo(str);
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(str) {
|
||||||
|
let date = new Date(str);
|
||||||
|
return new Intl.DateTimeFormat('default', {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric'
|
||||||
|
}).format(date);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAction(action) {
|
||||||
|
if(action === 'mark-read') {
|
||||||
|
axios.post('/i/admin/api/reports/remote/handle', {
|
||||||
|
id: this.model.id,
|
||||||
|
action: action,
|
||||||
|
}).then(res => {
|
||||||
|
console.log(res.data)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.$emit('refresh');
|
||||||
|
this.$emit('close');
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
swal({
|
||||||
|
title: 'Confirm',
|
||||||
|
text: 'Are you sure you want to ' + this.actionMap[action],
|
||||||
|
icon: 'warning',
|
||||||
|
buttons: true,
|
||||||
|
dangerMode: true,
|
||||||
|
}).then(res => {
|
||||||
|
if(res === true) {
|
||||||
|
axios.post('/i/admin/api/reports/remote/handle', {
|
||||||
|
id: this.model.id,
|
||||||
|
action: action,
|
||||||
|
}).finally(() => {
|
||||||
|
this.$emit('refresh');
|
||||||
|
this.$emit('close');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -146,6 +146,8 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
|
||||||
Route::post('instances/import-data', 'AdminController@importBackup');
|
Route::post('instances/import-data', 'AdminController@importBackup');
|
||||||
Route::get('reports/stats', 'AdminController@reportsStats');
|
Route::get('reports/stats', 'AdminController@reportsStats');
|
||||||
Route::get('reports/all', 'AdminController@reportsApiAll');
|
Route::get('reports/all', 'AdminController@reportsApiAll');
|
||||||
|
Route::get('reports/remote', 'AdminController@reportsApiRemote');
|
||||||
|
Route::post('reports/remote/handle', 'AdminController@reportsApiRemoteHandle');
|
||||||
Route::get('reports/get/{id}', 'AdminController@reportsApiGet');
|
Route::get('reports/get/{id}', 'AdminController@reportsApiGet');
|
||||||
Route::post('reports/handle', 'AdminController@reportsApiHandle');
|
Route::post('reports/handle', 'AdminController@reportsApiHandle');
|
||||||
Route::get('reports/spam/all', 'AdminController@reportsApiSpamAll');
|
Route::get('reports/spam/all', 'AdminController@reportsApiSpamAll');
|
||||||
|
|
Loading…
Reference in a new issue