Defer artwork comments from initial render

This commit is contained in:
2026-08-29 12:07:08 +02:00
parent 8a80aae21e
commit 4ab315e9d7
8 changed files with 267 additions and 138 deletions
@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Web;
@@ -8,16 +7,13 @@ use App\Enums\ReactionType;
use App\Http\Controllers\Controller;
use App\Http\Resources\ArtworkResource;
use App\Models\Artwork;
use App\Models\ArtworkComment;
use Illuminate\Support\Facades\DB;
use App\Services\ContentSanitizer;
use App\Services\ThumbnailPresenter;
use App\Services\ArtworkNavigationService;
use App\Services\ErrorSuggestionService;
use App\Services\GroupService;
use App\Services\Maturity\ArtworkMaturityService;
use App\Support\Seo\SeoFactory;
use App\Support\AvatarUrl;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -32,6 +28,7 @@ final class ArtworkPageController extends Controller
public function __construct(
private readonly GroupService $groups,
private readonly ArtworkMaturityService $maturity,
private readonly ArtworkNavigationService $navigation,
) {}
public function show(Request $request, int $id, ?string $slug = null): View|RedirectResponse|Response|InertiaResponse
@@ -98,12 +95,13 @@ final class ArtworkPageController extends Controller
]), 403);
}
// ── Step 2: full load with all relations ───────────────────────────
$artwork = Artwork::with(['user.profile', 'group.owner.profile', 'uploadedBy.profile', 'primaryAuthor.profile', 'contributors.user.profile', 'categories.contentType', 'categories.parent.contentType', 'tags', 'stats', 'awardStat'])
->where('id', $id)
->public()
->published()
->firstOrFail();
// ── Step 2: hydrate the already-validated model ────────────────────
// The initial withTrashed lookup is intentionally retained so that
// missing, deleted, private, and unpublished artwork keep their
// distinct responses. Reusing that model avoids a second identical
// primary-key lookup on the public path.
$artwork = $raw;
$artwork->load(['user.profile', 'group.owner.profile', 'uploadedBy.profile', 'primaryAuthor.profile', 'contributors.user.profile', 'categories.contentType', 'categories.parent.contentType', 'tags', 'stats', 'awardStat']);
$this->loadCategoryAncestors($artwork->categories);
@@ -119,12 +117,15 @@ final class ArtworkPageController extends Controller
], 301);
}
$thumbMd = ThumbnailPresenter::present($artwork, 'md');
$thumbLg = ThumbnailPresenter::present($artwork, 'lg');
$thumbXl = ThumbnailPresenter::present($artwork, 'xl');
$thumbSq = ThumbnailPresenter::present($artwork, 'sq');
$artworkData = (new ArtworkResource($artwork))->toArray($request);
$navigationData = $this->navigation->navigationFor($artwork);
// ArtworkResource is the canonical thumbnail projection. Reuse its
// exact values for page props and SEO instead of presenting each
// variant a second time in the controller.
$thumbMd = $artworkData['thumbs']['md'] ?? [];
$thumbLg = $artworkData['thumbs']['lg'] ?? [];
$thumbXl = $artworkData['thumbs']['xl'] ?? [];
$thumbSq = $artworkData['thumbs']['sq'] ?? [];
$groupSummary = null;
if ($artwork->group) {
@@ -206,84 +207,22 @@ final class ArtworkPageController extends Controller
->values()
->all();
$approvedComments = ArtworkComment::query()
->with('user.profile')
->where('artwork_id', $artwork->id)
->where('is_approved', true)
->orderBy('created_at')
->limit(500)
->get();
$commentsByParent = $approvedComments->groupBy(
static fn (ArtworkComment $comment): string => $comment->parent_id === null
? 'root'
: (string) $comment->parent_id
);
// Recursive helper to format a comment and its nested replies.
$formatComment = null;
$formatComment = function (ArtworkComment $c) use (&$formatComment, $commentsByParent): array {
/** @var Collection<int, ArtworkComment> $replies */
$replies = $commentsByParent->get((string) $c->id, collect());
$user = $c->user;
$userId = (int) ($c->user_id ?? 0);
$avatarHash = $user?->profile?->avatar_hash ?? null;
$canPublishLinks = (int) ($user?->level ?? 1) > 1 && strtolower((string) ($user?->rank ?? 'Newbie')) !== 'newbie';
$rawContent = (string) ($c->raw_content ?? $c->content ?? '');
$renderedContent = $c->rendered_content;
if (! is_string($renderedContent) || trim($renderedContent) === '') {
$renderedContent = $rawContent !== ''
? ContentSanitizer::render($rawContent)
: nl2br(e(strip_tags((string) ($c->content ?? ''))));
}
return [
'id' => $c->id,
'parent_id' => $c->parent_id,
'content' => html_entity_decode((string) $c->content, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'raw_content' => $c->raw_content ?? $c->content,
'rendered_content' => ContentSanitizer::sanitizeRenderedHtml($renderedContent, $canPublishLinks),
'created_at' => $c->created_at?->toIso8601String(),
'time_ago' => $c->created_at ? Carbon::parse($c->created_at)->diffForHumans() : null,
'user' => [
'id' => $userId,
'name' => $user?->name,
'username' => $user?->username,
'display' => $user?->username ?? $user?->name ?? 'User',
'profile_url' => $user?->username ? '/@' . $user->username : ($userId > 0 ? '/profile/' . $userId : null),
'avatar_url' => $avatarHash !== null
? AvatarUrl::forUser($userId, $avatarHash, 64)
: AvatarUrl::default(),
'level' => (int) ($user?->level ?? 1),
'rank' => (string) ($user?->rank ?? 'Newbie'),
],
'replies' => $replies->map($formatComment)->values()->all(),
];
};
$comments = $commentsByParent
->get('root', collect())
->map($formatComment)
->values()
->all();
$canReadSession = $request->hasSession() && ! $request->attributes->get('skinbase.session_skipped');
$viewerId = ($canReadSession && $request->user() !== null) ? (int) $request->user()->id : null;
$userId = ($canReadSession && $request->user() !== null) ? (int) $request->user()->id : null;
return Inertia::render('ArtworkPage', [
'artwork' => $artworkData,
'navigation' => $navigationData,
'presentMd' => $thumbMd,
'presentLg' => $thumbLg,
'presentXl' => $thumbXl,
'presentSq' => $thumbSq,
'related' => $related,
'canonicalUrl' => $canonical,
'comments' => $comments,
'groupSummary' => $groupSummary,
'isAuthenticated' => $userId !== null,
'reactionTotals' => $this->artworkReactionTotals((int) $artwork->id, $userId),
'isAuthenticated' => $viewerId !== null,
'reactionTotals' => $this->artworkReactionTotals((int) $artwork->id, $viewerId),
'seo' => $seo,
])->rootView('artworks.show');
}