163 lines
5.5 KiB
PHP
163 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\NewsArticleComment;
|
|
use App\Models\User;
|
|
use App\Services\News\NewsArticleCommentService;
|
|
use App\Support\AvatarUrl;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use cPad\Plugins\News\Models\NewsArticle;
|
|
|
|
final class NewsArticleCommentController extends Controller
|
|
{
|
|
private const MAX_LENGTH = 4_000;
|
|
|
|
public function __construct(private readonly NewsArticleCommentService $comments)
|
|
{
|
|
}
|
|
|
|
public function index(Request $request, int $articleId): JsonResponse
|
|
{
|
|
$article = $this->resolveArticle($articleId);
|
|
$page = max(1, (int) $request->query('page', 1));
|
|
$perPage = 20;
|
|
|
|
$comments = NewsArticleComment::query()
|
|
->with(['user.profile', 'visibleReplies'])
|
|
->where('article_id', $article->id)
|
|
->where('status', 'visible')
|
|
->whereNull('parent_id')
|
|
->orderByDesc('created_at')
|
|
->orderByDesc('id')
|
|
->paginate($perPage, ['*'], 'page', $page);
|
|
|
|
$viewer = $request->user();
|
|
$items = $comments->getCollection()->map(
|
|
fn (NewsArticleComment $comment): array => $this->formatComment($comment, $viewer, $article, true)
|
|
);
|
|
|
|
return response()->json([
|
|
'data' => $items,
|
|
'meta' => [
|
|
'current_page' => $comments->currentPage(),
|
|
'last_page' => $comments->lastPage(),
|
|
'total' => $comments->total(),
|
|
'per_page' => $comments->perPage(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request, int $articleId): JsonResponse
|
|
{
|
|
$article = $this->resolveArticle($articleId);
|
|
|
|
$data = $request->validate([
|
|
'content' => ['required', 'string', 'min:2', 'max:' . self::MAX_LENGTH],
|
|
'parent_id' => ['nullable', 'integer', 'exists:news_article_comments,id'],
|
|
]);
|
|
|
|
$parent = null;
|
|
|
|
if (! empty($data['parent_id'])) {
|
|
$parent = NewsArticleComment::query()
|
|
->where('article_id', $article->id)
|
|
->where('status', 'visible')
|
|
->find((int) $data['parent_id']);
|
|
|
|
if (! $parent) {
|
|
return response()->json([
|
|
'errors' => [
|
|
'content' => ['The comment you are replying to is no longer available.'],
|
|
],
|
|
], 422);
|
|
}
|
|
}
|
|
|
|
$comment = $this->comments->create($article, $request->user(), (string) $data['content'], $parent);
|
|
|
|
return response()->json([
|
|
'data' => $this->formatComment($comment, $request->user(), $article, false),
|
|
], 201);
|
|
}
|
|
|
|
public function destroy(Request $request, int $articleId, int $commentId): JsonResponse
|
|
{
|
|
$article = $this->resolveArticle($articleId);
|
|
|
|
$comment = NewsArticleComment::query()
|
|
->where('article_id', $article->id)
|
|
->findOrFail($commentId);
|
|
|
|
$this->comments->delete($comment, $request->user());
|
|
|
|
return response()->json([
|
|
'message' => 'Comment deleted.',
|
|
]);
|
|
}
|
|
|
|
private function resolveArticle(int $articleId): NewsArticle
|
|
{
|
|
return NewsArticle::query()
|
|
->published()
|
|
->whereKey($articleId)
|
|
->firstOrFail();
|
|
}
|
|
|
|
private function formatComment(NewsArticleComment $comment, ?User $viewer, NewsArticle $article, bool $includeReplies = false): array
|
|
{
|
|
$user = $comment->user;
|
|
$displayName = $user?->username ?? $user?->name ?? $comment->author_name ?? 'Former member';
|
|
$avatarHash = $user?->profile?->avatar_hash ?? null;
|
|
|
|
$data = [
|
|
'id' => $comment->id,
|
|
'parent_id' => $comment->parent_id,
|
|
'raw_content' => (string) ($comment->body ?? ''),
|
|
'rendered_content' => $comment->getDisplayHtml(),
|
|
'created_at' => $comment->created_at?->toIso8601String(),
|
|
'time_ago' => $comment->created_at ? Carbon::parse($comment->created_at)->diffForHumans() : null,
|
|
'can_delete' => $this->canDelete($comment, $article, $viewer),
|
|
'user' => [
|
|
'id' => $user?->id,
|
|
'username' => $user?->username,
|
|
'display' => $displayName,
|
|
'profile_url' => $user?->username ? '/@' . $user->username : null,
|
|
'avatar_url' => $user ? AvatarUrl::forUser((int) $user->id, $avatarHash, 64) : null,
|
|
'level' => (int) ($user?->level ?? 1),
|
|
'rank' => (string) ($user?->rank ?? 'Newbie'),
|
|
],
|
|
'replies' => [],
|
|
];
|
|
|
|
if ($includeReplies) {
|
|
$replies = $comment->relationLoaded('visibleReplies')
|
|
? $comment->visibleReplies
|
|
: collect();
|
|
|
|
$data['replies'] = $replies
|
|
->map(fn (NewsArticleComment $reply): array => $this->formatComment($reply, $viewer, $article, true))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function canDelete(NewsArticleComment $comment, NewsArticle $article, ?User $viewer): bool
|
|
{
|
|
if (! $viewer) {
|
|
return false;
|
|
}
|
|
|
|
return (int) $comment->user_id === (int) $viewer->id
|
|
|| (int) $article->author_id === (int) $viewer->id
|
|
|| $viewer->isAdmin()
|
|
|| $viewer->isModerator();
|
|
}
|
|
} |