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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user