Limit anonymous community activity queries to the latest page.

Skip unused activity sources and historical over-fetching for guests while keeping authenticated feeds filterable.
This commit is contained in:
2026-09-20 14:50:58 +02:00
parent 5d703b8da4
commit 0b6f3a95e3
+61 -12
View File
@@ -33,8 +33,10 @@ final class CommunityActivityService
public function getFeed(?User $viewer, string $filter = self::FILTER_ALL, int $page = 1, int $perPage = self::DEFAULT_PER_PAGE, ?int $actorUserId = null): array
{
$normalizedFilter = $this->normalizeFilter($filter);
$resolvedPage = max(1, $page);
$resolvedPerPage = max(1, min(50, $perPage));
$resolvedPage = $viewer ? max(1, $page) : 1;
$resolvedPerPage = $viewer
? max(1, min(50, $perPage))
: 20;
$cacheKey = sprintf(
'community_activity:%s:%d:%d:%d:%d',
@@ -68,20 +70,55 @@ final class CommunityActivityService
private function buildFeed(?User $viewer, string $filter, int $page, int $perPage, ?int $actorUserId): array
{
$sourceLimit = max(80, $page * $perPage * 6);
// Guests only receive the latest page. Since every source is ordered
// newest-first, one page per source is sufficient to produce the
// globally newest page after merging, while avoiding historical
// over-fetching for anonymous traffic.
$sourceLimit = $viewer === null
? $perPage
: max(80, $page * $perPage * 6);
$followingIds = $filter === self::FILTER_FOLLOWING && $viewer
? $viewer->following()->pluck('users.id')->map(fn ($id) => (int) $id)->all()
: [];
$actorIds = match ($filter) {
self::FILTER_FOLLOWING => $followingIds,
self::FILTER_MY => $viewer ? [(int) $viewer->id] : [],
default => null,
};
$commentModels = $this->fetchCommentModels($sourceLimit, repliesOnly: false);
$replyModels = $this->fetchCommentModels($sourceLimit, repliesOnly: true);
$reactionModels = $this->fetchReactionModels($sourceLimit);
$recordedActivities = $this->fetchRecordedActivities($sourceLimit);
if ($actorUserId !== null) {
$actorIds = $actorIds === null
? [(int) $actorUserId]
: array_values(array_intersect($actorIds, [(int) $actorUserId]));
}
// A comments/replies feed can only contain its matching comment type.
// Avoid loading and hydrating the other activity sources only to discard
// them in the merge filter below. Actor-scoped filters also constrain
// each source query while preserving the final in-memory safety filter.
$commentModels = in_array($filter, [self::FILTER_ALL, self::FILTER_COMMENTS, self::FILTER_FOLLOWING, self::FILTER_MY], true)
? $this->fetchCommentModels($sourceLimit, repliesOnly: false, actorIds: $actorIds)
: collect();
$replyModels = in_array($filter, [self::FILTER_ALL, self::FILTER_REPLIES, self::FILTER_FOLLOWING, self::FILTER_MY], true)
? $this->fetchCommentModels($sourceLimit, repliesOnly: true, actorIds: $actorIds)
: collect();
$reactionModels = in_array($filter, [self::FILTER_ALL, self::FILTER_FOLLOWING, self::FILTER_MY], true)
? $this->fetchReactionModels($sourceLimit, actorIds: $actorIds)
: collect();
$recordedActivities = in_array($filter, [self::FILTER_ALL, self::FILTER_FOLLOWING, self::FILTER_MY], true)
? $this->fetchRecordedActivities($sourceLimit, actorIds: $actorIds)
: collect();
$commentActivities = $commentModels->map(fn (ArtworkComment $comment) => $this->mapCommentActivity($comment, 'comment'));
$replyActivities = $replyModels->map(fn (ArtworkComment $comment) => $this->mapCommentActivity($comment, 'reply'));
$reactionActivities = $reactionModels->map(fn (CommentReaction $reaction) => $this->mapReactionActivity($reaction));
$mentionActivities = $this->fetchMentionActivities($sourceLimit);
$mentionActivities = in_array($filter, [self::FILTER_ALL, self::FILTER_FOLLOWING, self::FILTER_MY], true)
? $this->fetchMentionActivities(
$sourceLimit,
actorIds: $actorIds,
mentionedUserId: $filter === self::FILTER_MY ? $viewer?->id : null,
)
: collect();
$merged = $recordedActivities
->concat($commentActivities)
@@ -140,7 +177,7 @@ final class CommunityActivityService
];
}
private function fetchRecordedActivities(int $limit): Collection
private function fetchRecordedActivities(int $limit, ?array $actorIds = null): Collection
{
$events = ActivityEvent::query()
->select(['id', 'actor_id', 'type', 'target_type', 'target_id', 'meta', 'created_at'])
@@ -153,6 +190,7 @@ final class CommunityActivityService
},
])
->whereHas('actor', fn ($query) => $query->where('is_active', true)->whereNull('deleted_at'))
->when($actorIds !== null, fn ($query) => $query->whereIn('actor_id', $actorIds))
->latest('created_at')
->limit($limit)
->get();
@@ -223,7 +261,7 @@ final class CommunityActivityService
->values();
}
private function fetchCommentModels(int $limit, bool $repliesOnly): Collection
private function fetchCommentModels(int $limit, bool $repliesOnly, ?array $actorIds = null): Collection
{
return ArtworkComment::query()
->select([
@@ -250,6 +288,7 @@ final class CommunityActivityService
])
->where('is_approved', true)
->whereNull('deleted_at')
->when($actorIds !== null, fn ($query) => $query->whereIn('user_id', $actorIds))
->when($repliesOnly, fn ($query) => $query->whereNotNull('parent_id'), fn ($query) => $query->whereNull('parent_id'))
->whereHas('user', function ($query) {
$query->where('is_active', true)->whereNull('deleted_at');
@@ -262,7 +301,7 @@ final class CommunityActivityService
->get();
}
private function fetchReactionModels(int $limit): Collection
private function fetchReactionModels(int $limit, ?array $actorIds = null): Collection
{
return CommentReaction::query()
->select(['id', 'comment_id', 'user_id', 'reaction', 'created_at'])
@@ -292,6 +331,7 @@ final class CommunityActivityService
->whereHas('user', function ($query) {
$query->where('is_active', true)->whereNull('deleted_at');
})
->when($actorIds !== null, fn ($query) => $query->whereIn('user_id', $actorIds))
->whereHas('comment', function ($query) {
$query
->where('is_approved', true)
@@ -395,7 +435,7 @@ final class CommunityActivityService
];
}
private function fetchMentionActivities(int $limit): Collection
private function fetchMentionActivities(int $limit, ?array $actorIds = null, ?int $mentionedUserId = null): Collection
{
if (! Schema::hasTable('user_mentions')) {
return collect();
@@ -403,6 +443,15 @@ final class CommunityActivityService
return UserMention::query()
->select(['id', 'user_id', 'mentioned_user_id', 'artwork_id', 'comment_id', 'created_at'])
->when($actorIds !== null, function ($query) use ($actorIds, $mentionedUserId): void {
$query->where(function ($scope) use ($actorIds, $mentionedUserId): void {
$scope->whereIn('user_id', $actorIds);
if ($mentionedUserId !== null) {
$scope->orWhere('mentioned_user_id', $mentionedUserId);
}
});
})
->with([
'actor' => function ($query) {
$query