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(); } }