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:
test
2026-09-20 14:50:58 +02:00
parent c247b8f652
commit dae5a49c4b
+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 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); $normalizedFilter = $this->normalizeFilter($filter);
$resolvedPage = max(1, $page); $resolvedPage = $viewer ? max(1, $page) : 1;
$resolvedPerPage = max(1, min(50, $perPage)); $resolvedPerPage = $viewer
? max(1, min(50, $perPage))
: 20;
$cacheKey = sprintf( $cacheKey = sprintf(
'community_activity:%s:%d:%d:%d:%d', '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 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 $followingIds = $filter === self::FILTER_FOLLOWING && $viewer
? $viewer->following()->pluck('users.id')->map(fn ($id) => (int) $id)->all() ? $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); if ($actorUserId !== null) {
$replyModels = $this->fetchCommentModels($sourceLimit, repliesOnly: true); $actorIds = $actorIds === null
$reactionModels = $this->fetchReactionModels($sourceLimit); ? [(int) $actorUserId]
$recordedActivities = $this->fetchRecordedActivities($sourceLimit); : 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')); $commentActivities = $commentModels->map(fn (ArtworkComment $comment) => $this->mapCommentActivity($comment, 'comment'));
$replyActivities = $replyModels->map(fn (ArtworkComment $comment) => $this->mapCommentActivity($comment, 'reply')); $replyActivities = $replyModels->map(fn (ArtworkComment $comment) => $this->mapCommentActivity($comment, 'reply'));
$reactionActivities = $reactionModels->map(fn (CommentReaction $reaction) => $this->mapReactionActivity($reaction)); $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 $merged = $recordedActivities
->concat($commentActivities) ->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() $events = ActivityEvent::query()
->select(['id', 'actor_id', 'type', 'target_type', 'target_id', 'meta', 'created_at']) ->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')) ->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') ->latest('created_at')
->limit($limit) ->limit($limit)
->get(); ->get();
@@ -223,7 +261,7 @@ final class CommunityActivityService
->values(); ->values();
} }
private function fetchCommentModels(int $limit, bool $repliesOnly): Collection private function fetchCommentModels(int $limit, bool $repliesOnly, ?array $actorIds = null): Collection
{ {
return ArtworkComment::query() return ArtworkComment::query()
->select([ ->select([
@@ -250,6 +288,7 @@ final class CommunityActivityService
]) ])
->where('is_approved', true) ->where('is_approved', true)
->whereNull('deleted_at') ->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')) ->when($repliesOnly, fn ($query) => $query->whereNotNull('parent_id'), fn ($query) => $query->whereNull('parent_id'))
->whereHas('user', function ($query) { ->whereHas('user', function ($query) {
$query->where('is_active', true)->whereNull('deleted_at'); $query->where('is_active', true)->whereNull('deleted_at');
@@ -262,7 +301,7 @@ final class CommunityActivityService
->get(); ->get();
} }
private function fetchReactionModels(int $limit): Collection private function fetchReactionModels(int $limit, ?array $actorIds = null): Collection
{ {
return CommentReaction::query() return CommentReaction::query()
->select(['id', 'comment_id', 'user_id', 'reaction', 'created_at']) ->select(['id', 'comment_id', 'user_id', 'reaction', 'created_at'])
@@ -292,6 +331,7 @@ final class CommunityActivityService
->whereHas('user', function ($query) { ->whereHas('user', function ($query) {
$query->where('is_active', true)->whereNull('deleted_at'); $query->where('is_active', true)->whereNull('deleted_at');
}) })
->when($actorIds !== null, fn ($query) => $query->whereIn('user_id', $actorIds))
->whereHas('comment', function ($query) { ->whereHas('comment', function ($query) {
$query $query
->where('is_approved', true) ->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')) { if (! Schema::hasTable('user_mentions')) {
return collect(); return collect();
@@ -403,6 +443,15 @@ final class CommunityActivityService
return UserMention::query() return UserMention::query()
->select(['id', 'user_id', 'mentioned_user_id', 'artwork_id', 'comment_id', 'created_at']) ->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([ ->with([
'actor' => function ($query) { 'actor' => function ($query) {
$query $query