189 lines
8.2 KiB
PHP
189 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Artwork;
|
|
use App\Models\ArtworkComment;
|
|
use App\Services\ContentSanitizer;
|
|
use App\Services\LegacySmileyMapper;
|
|
use App\Support\AvatarUrl;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Gate;
|
|
|
|
/**
|
|
* Artwork comment CRUD.
|
|
*
|
|
* POST /api/artworks/{artworkId}/comments → store
|
|
* PUT /api/artworks/{artworkId}/comments/{id} → update (own comment)
|
|
* DELETE /api/artworks/{artworkId}/comments/{id} → delete (own or admin)
|
|
* GET /api/artworks/{artworkId}/comments → list (paginated)
|
|
*/
|
|
class ArtworkCommentController extends Controller
|
|
{
|
|
private const MAX_LENGTH = 10_000;
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// List
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public function index(Request $request, int $artworkId): JsonResponse
|
|
{
|
|
$artwork = Artwork::public()->published()->findOrFail($artworkId);
|
|
|
|
$page = max(1, (int) $request->query('page', 1));
|
|
$perPage = 20;
|
|
|
|
$comments = ArtworkComment::with(['user', 'user.profile'])
|
|
->where('artwork_id', $artwork->id)
|
|
->where('is_approved', true)
|
|
->orderByDesc('created_at')
|
|
->paginate($perPage, ['*'], 'page', $page);
|
|
|
|
$userId = $request->user()?->id;
|
|
$items = $comments->getCollection()->map(fn ($c) => $this->formatComment($c, $userId));
|
|
|
|
return response()->json([
|
|
'data' => $items,
|
|
'meta' => [
|
|
'current_page' => $comments->currentPage(),
|
|
'last_page' => $comments->lastPage(),
|
|
'total' => $comments->total(),
|
|
'per_page' => $comments->perPage(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Store
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public function store(Request $request, int $artworkId): JsonResponse
|
|
{
|
|
$artwork = Artwork::public()->published()->findOrFail($artworkId);
|
|
|
|
$request->validate([
|
|
'content' => ['required', 'string', 'min:1', 'max:' . self::MAX_LENGTH],
|
|
]);
|
|
|
|
$raw = $request->input('content');
|
|
|
|
// Validate markdown-lite content
|
|
$errors = ContentSanitizer::validate($raw);
|
|
if ($errors) {
|
|
return response()->json(['errors' => ['content' => $errors]], 422);
|
|
}
|
|
|
|
$rendered = ContentSanitizer::render($raw);
|
|
|
|
$comment = ArtworkComment::create([
|
|
'artwork_id' => $artwork->id,
|
|
'user_id' => $request->user()->id,
|
|
'content' => $raw, // legacy column (plain text fallback)
|
|
'raw_content' => $raw,
|
|
'rendered_content' => $rendered,
|
|
'is_approved' => true, // auto-approve; extend with moderation as needed
|
|
]);
|
|
|
|
// Bust the comments cache for this user's 'all' feed
|
|
Cache::forget('comments.latest.all.page1');
|
|
|
|
$comment->load(['user', 'user.profile']);
|
|
|
|
// Record activity event (fire-and-forget; never break the response)
|
|
try {
|
|
\App\Models\ActivityEvent::record(
|
|
actorId: $request->user()->id,
|
|
type: \App\Models\ActivityEvent::TYPE_COMMENT,
|
|
targetType: \App\Models\ActivityEvent::TARGET_ARTWORK,
|
|
targetId: $artwork->id,
|
|
);
|
|
} catch (\Throwable) {}
|
|
|
|
return response()->json(['data' => $this->formatComment($comment, $request->user()->id)], 201);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Update
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public function update(Request $request, int $artworkId, int $commentId): JsonResponse
|
|
{
|
|
$comment = ArtworkComment::where('artwork_id', $artworkId)
|
|
->findOrFail($commentId);
|
|
|
|
Gate::authorize('update', $comment);
|
|
|
|
$request->validate([
|
|
'content' => ['required', 'string', 'min:1', 'max:' . self::MAX_LENGTH],
|
|
]);
|
|
|
|
$raw = $request->input('content');
|
|
$errors = ContentSanitizer::validate($raw);
|
|
if ($errors) {
|
|
return response()->json(['errors' => ['content' => $errors]], 422);
|
|
}
|
|
|
|
$rendered = ContentSanitizer::render($raw);
|
|
|
|
$comment->update([
|
|
'content' => $raw,
|
|
'raw_content' => $raw,
|
|
'rendered_content' => $rendered,
|
|
]);
|
|
|
|
Cache::forget('comments.latest.all.page1');
|
|
|
|
$comment->load(['user', 'user.profile']);
|
|
|
|
return response()->json(['data' => $this->formatComment($comment, $request->user()->id)]);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Delete
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public function destroy(Request $request, int $artworkId, int $commentId): JsonResponse
|
|
{
|
|
$comment = ArtworkComment::where('artwork_id', $artworkId)->findOrFail($commentId);
|
|
|
|
Gate::authorize('delete', $comment);
|
|
|
|
$comment->delete();
|
|
Cache::forget('comments.latest.all.page1');
|
|
|
|
return response()->json(['message' => 'Comment deleted.'], 200);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Helpers
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
private function formatComment(ArtworkComment $c, ?int $currentUserId): array
|
|
{
|
|
$user = $c->user;
|
|
$userId = (int) ($c->user_id ?? 0);
|
|
$avatarHash = $user?->profile?->avatar_hash ?? null;
|
|
|
|
return [
|
|
'id' => $c->id,
|
|
'raw_content' => $c->raw_content ?? $c->content,
|
|
'rendered_content' => $c->rendered_content ?? e(strip_tags($c->content ?? '')),
|
|
'created_at' => $c->created_at?->toIso8601String(),
|
|
'time_ago' => $c->created_at ? Carbon::parse($c->created_at)->diffForHumans() : null,
|
|
'can_edit' => $currentUserId === $userId,
|
|
'can_delete' => $currentUserId === $userId,
|
|
'user' => [
|
|
'id' => $userId,
|
|
'username' => $user?->username,
|
|
'display' => $user?->username ?? $user?->name ?? 'User',
|
|
'profile_url' => $user?->username ? '/@' . $user->username : '/profile/' . $userId,
|
|
'avatar_url' => AvatarUrl::forUser($userId, $avatarHash, 64),
|
|
],
|
|
];
|
|
}
|
|
}
|