fedi-feed-router/app/Livewire/Articles.php
2026-08-13 21:36:57 +02:00

232 lines
6.6 KiB
PHP

<?php
namespace App\Livewire;
use App\Enums\ActivityTypeEnum;
use App\Enums\ApprovalStatusEnum;
use App\Events\ActivityLogged;
use App\Jobs\ArticleDiscoveryJob;
use App\Models\Feed;
use App\Models\RouteArticle;
use App\Support\PendingFeedGroup;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Livewire\Component;
use Livewire\WithPagination;
class Articles extends Component
{
use WithPagination;
public string $tab = 'pending';
public string $search = '';
public ?int $feedId = null;
public bool $isRefreshing = false;
/** @var array<int, bool> */
public array $expandedFeeds = [];
public function setTab(string $tab): void
{
$this->tab = $tab;
$this->search = '';
$this->expandedFeeds = [];
$this->resetPage();
}
public function updatedFeedId(): void
{
$this->expandedFeeds = [];
$this->resetPage();
}
public function toggleFeed(int $feedId): void
{
if (isset($this->expandedFeeds[$feedId])) {
unset($this->expandedFeeds[$feedId]);
return;
}
$this->expandedFeeds[$feedId] = true;
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function approve(int $routeArticleId): void
{
RouteArticle::findOrFail($routeArticleId)->approve();
}
public function reject(int $routeArticleId): void
{
RouteArticle::findOrFail($routeArticleId)->reject();
}
public function restore(int $routeArticleId): void
{
$routeArticle = RouteArticle::findOrFail($routeArticleId);
$routeArticle->update([
'approval_status' => ApprovalStatusEnum::PENDING,
'decided_at' => null,
]);
}
public function clear(): void
{
$feed = $this->feedId !== null ? Feed::find($this->feedId) : null;
$cleared = $this->clearableQuery()->update([
'approval_status' => ApprovalStatusEnum::REJECTED,
'decided_at' => now(),
]);
if ($cleared > 0) {
ActivityLogged::dispatch(
ActivityTypeEnum::REJECT,
$feed !== null
? "Cleared {$cleared} pending articles from {$feed->name}"
: "Cleared {$cleared} pending articles",
['cleared' => $cleared],
$feed,
);
}
$this->expandedFeeds = [];
}
/**
* @return Builder<RouteArticle>
*/
private function clearableQuery(): Builder
{
return RouteArticle::query()
->where('approval_status', ApprovalStatusEnum::PENDING)
->when($this->feedId !== null, fn (Builder $q) => $q->where('feed_id', $this->feedId));
}
public function refresh(): void
{
$this->isRefreshing = true;
ArticleDiscoveryJob::dispatch();
$this->dispatch('refresh-started');
}
public function retryPublish(int $routeArticleId): void
{
$routeArticle = RouteArticle::failed()->find($routeArticleId);
if (! $routeArticle instanceof RouteArticle) {
return;
}
$routeArticle->clearPublishFailure();
ActivityLogged::dispatch(
ActivityTypeEnum::PUBLISH,
"Queued \"{$routeArticle->article->title}\" for another publish attempt",
['route_article_id' => $routeArticle->id],
$routeArticle->article,
);
}
public function render(): View
{
$pendingCount = RouteArticle::where('approval_status', ApprovalStatusEnum::PENDING)->count();
$failedCount = RouteArticle::failed()->count();
if ($this->tab === 'pending') {
return view('livewire.articles', [
'routeArticles' => null,
'pendingFeeds' => $this->pendingFeeds(),
'feedOptions' => $this->feedOptions(),
'pendingCount' => $pendingCount,
'failedCount' => $failedCount,
'clearableCount' => $this->clearableQuery()->count(),
])->layout('layouts.app');
}
$query = RouteArticle::with(['article.feed', 'feed', 'platformChannel'])
->orderBy('created_at', 'desc');
if ($this->tab === 'failed') {
$query->failed();
}
if ($this->feedId !== null) {
$query->where('feed_id', $this->feedId);
}
if ($this->search !== '') {
$search = $this->search;
$query->whereHas('article', function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}
return view('livewire.articles', [
'routeArticles' => $query->paginate(15),
'pendingFeeds' => null,
'feedOptions' => $this->feedOptions(),
'pendingCount' => $pendingCount,
'failedCount' => $failedCount,
'clearableCount' => 0,
])->layout('layouts.app');
}
/**
* @return Collection<int, PendingFeedGroup>
*/
private function pendingFeeds(): Collection
{
$counts = RouteArticle::query()
->selectRaw('feed_id, COUNT(*) as aggregate')
->where('approval_status', ApprovalStatusEnum::PENDING)
->when($this->feedId !== null, fn ($q) => $q->where('feed_id', $this->feedId))
->groupBy('feed_id')
->pluck('aggregate', 'feed_id');
return Feed::whereIn('id', $counts->keys())->get()
->map(fn (Feed $feed): PendingFeedGroup => new PendingFeedGroup(
$feed,
(int) $counts->get($feed->id, 0),
isset($this->expandedFeeds[$feed->id])
? $this->routeArticlesForFeed($feed->id)
: null,
))
->sortByDesc('count')
->values();
}
/**
* @return EloquentCollection<int, Feed>
*/
private function feedOptions(): EloquentCollection
{
return Feed::whereIn('id', RouteArticle::query()->select('feed_id')->distinct())
->orderBy('name')
->get();
}
/**
* @return EloquentCollection<int, RouteArticle>
*/
private function routeArticlesForFeed(int $feedId): EloquentCollection
{
return RouteArticle::with(['article.feed', 'feed', 'platformChannel'])
->where('approval_status', ApprovalStatusEnum::PENDING)
->where('feed_id', $feedId)
->orderBy('created_at', 'desc')
->get();
}
}