Add news article comments and reactions
This commit is contained in:
163
app/Http/Controllers/Api/NewsArticleCommentController.php
Normal file
163
app/Http/Controllers/Api/NewsArticleCommentController.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\ArtworkReaction;
|
||||
use App\Models\CommentReaction;
|
||||
use App\Models\NewsArticleComment;
|
||||
use App\Models\NewsArticleCommentReaction;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -77,6 +79,33 @@ class ReactionController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function newsCommentReactions(Request $request, int $commentId): JsonResponse
|
||||
{
|
||||
$this->resolveVisibleNewsComment($commentId);
|
||||
|
||||
return response()->json([
|
||||
'entity_type' => 'news-comment',
|
||||
'entity_id' => $commentId,
|
||||
'totals' => $this->getTotals('news_article_comment_reactions', ['comment_id' => $commentId], $request->user()?->id),
|
||||
]);
|
||||
}
|
||||
|
||||
public function toggleNewsCommentReaction(Request $request, int $commentId): JsonResponse
|
||||
{
|
||||
$this->resolveVisibleNewsComment($commentId);
|
||||
$slug = $this->validateReactionSlug($request);
|
||||
|
||||
return $this->toggle(
|
||||
model: new NewsArticleCommentReaction(),
|
||||
where: ['comment_id' => $commentId, 'user_id' => $request->user()->id, 'reaction' => $slug],
|
||||
countWhere: ['comment_id' => $commentId],
|
||||
entityId: $commentId,
|
||||
entityType: 'news-comment',
|
||||
userId: $request->user()->id,
|
||||
slug: $slug,
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Shared internals
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -189,4 +218,13 @@ class ReactionController extends Controller
|
||||
throw new ModelNotFoundException("No [{$table}] record found with id [{$id}].");
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveVisibleNewsComment(int $commentId): NewsArticleComment
|
||||
{
|
||||
return NewsArticleComment::query()
|
||||
->where('id', $commentId)
|
||||
->where('status', 'visible')
|
||||
->whereHas('article', fn ($query) => $query->published())
|
||||
->firstOrFail();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user