optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -3,8 +3,11 @@
namespace App\Services;
use App\Models\User;
use App\Notifications\UserFollowedNotification;
use App\Services\Activity\UserActivityService;
use App\Events\Achievements\AchievementCheckRequested;
use App\Services\FollowAnalyticsService;
use App\Support\AvatarUrl;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
/**
@@ -19,7 +22,12 @@ use Illuminate\Support\Facades\DB;
*/
final class FollowService
{
public function __construct(private readonly XPService $xp) {}
public function __construct(
private readonly XPService $xp,
private readonly NotificationService $notifications,
private readonly FollowAnalyticsService $analytics,
) {
}
/**
* Follow $targetId on behalf of $actorId.
@@ -66,12 +74,17 @@ final class FollowService
);
} catch (\Throwable) {}
try {
app(UserActivityService::class)->logFollow($actorId, $targetId);
} catch (\Throwable) {}
$targetUser = User::query()->find($targetId);
$actorUser = User::query()->find($actorId);
if ($targetUser && $actorUser) {
$targetUser->notify(new UserFollowedNotification($actorUser));
$this->notifications->notifyUserFollowed($targetUser->loadMissing('profile'), $actorUser->loadMissing('profile'));
}
$this->analytics->recordFollow($actorId, $targetId);
$this->xp->awardFollowerReceived($targetId, $actorId);
event(new AchievementCheckRequested($targetId));
}
@@ -108,6 +121,10 @@ final class FollowService
$this->decrementCounter($targetId, 'followers_count');
});
if ($deleted) {
$this->analytics->recordUnfollow($actorId, $targetId);
}
return $deleted;
}
@@ -143,6 +160,60 @@ final class FollowService
->value('followers_count');
}
public function followingCount(int $userId): int
{
return (int) DB::table('user_statistics')
->where('user_id', $userId)
->value('following_count');
}
public function getMutualFollowers(int $userA, int $userB, int $limit = 13): array
{
$rows = DB::table('user_followers as left_follow')
->join('user_followers as right_follow', 'right_follow.follower_id', '=', 'left_follow.follower_id')
->join('users as u', 'u.id', '=', 'left_follow.follower_id')
->leftJoin('user_profiles as up', 'up.user_id', '=', 'u.id')
->where('left_follow.user_id', $userA)
->where('right_follow.user_id', $userB)
->where('left_follow.follower_id', '!=', $userA)
->where('left_follow.follower_id', '!=', $userB)
->whereNull('u.deleted_at')
->where('u.is_active', true)
->orderByDesc('left_follow.created_at')
->limit(max(1, $limit))
->select(['u.id', 'u.username', 'u.name', 'up.avatar_hash'])
->get();
return $this->mapUsers($rows);
}
public function relationshipContext(int $viewerId, int $targetId): array
{
if ($viewerId === $targetId) {
return [
'follower_overlap' => null,
'shared_following' => null,
'mutual_followers' => [
'count' => 0,
'users' => [],
],
];
}
$followerOverlap = $this->buildFollowerOverlapSummary($viewerId, $targetId);
$sharedFollowing = $this->buildSharedFollowingSummary($viewerId, $targetId);
$mutualFollowers = $this->getMutualFollowers($viewerId, $targetId, 6);
return [
'follower_overlap' => $followerOverlap,
'shared_following' => $sharedFollowing,
'mutual_followers' => [
'count' => count($mutualFollowers),
'users' => $mutualFollowers,
],
];
}
// ─── Private helpers ─────────────────────────────────────────────────────
private function incrementCounter(int $userId, string $column): void
@@ -167,4 +238,89 @@ final class FollowService
'updated_at' => now(),
]);
}
private function buildFollowerOverlapSummary(int $viewerId, int $targetId): ?array
{
$preview = DB::table('user_followers as viewer_following')
->join('user_followers as target_followers', 'target_followers.follower_id', '=', 'viewer_following.user_id')
->join('users as u', 'u.id', '=', 'viewer_following.user_id')
->leftJoin('user_profiles as up', 'up.user_id', '=', 'u.id')
->where('viewer_following.follower_id', $viewerId)
->where('target_followers.user_id', $targetId)
->whereNull('u.deleted_at')
->where('u.is_active', true)
->orderByDesc('target_followers.created_at')
->limit(3)
->select(['u.id', 'u.username', 'u.name', 'up.avatar_hash'])
->get();
if ($preview->isEmpty()) {
return null;
}
$count = DB::table('user_followers as viewer_following')
->join('user_followers as target_followers', 'target_followers.follower_id', '=', 'viewer_following.user_id')
->where('viewer_following.follower_id', $viewerId)
->where('target_followers.user_id', $targetId)
->count();
$lead = $preview->first();
$label = $count > 1
? sprintf('Followed by %s and %d other%s', $lead->username ?? $lead->name ?? 'someone', $count - 1, $count - 1 === 1 ? '' : 's')
: sprintf('Followed by %s', $lead->username ?? $lead->name ?? 'someone');
return [
'count' => (int) $count,
'label' => $label,
'users' => $this->mapUsers($preview),
];
}
private function buildSharedFollowingSummary(int $viewerId, int $targetId): ?array
{
$preview = DB::table('user_followers as viewer_following')
->join('user_followers as target_following', 'target_following.user_id', '=', 'viewer_following.user_id')
->join('users as u', 'u.id', '=', 'viewer_following.user_id')
->leftJoin('user_profiles as up', 'up.user_id', '=', 'u.id')
->where('viewer_following.follower_id', $viewerId)
->where('target_following.follower_id', $targetId)
->whereNull('u.deleted_at')
->where('u.is_active', true)
->orderByDesc('viewer_following.created_at')
->limit(3)
->select(['u.id', 'u.username', 'u.name', 'up.avatar_hash'])
->get();
if ($preview->isEmpty()) {
return null;
}
$count = DB::table('user_followers as viewer_following')
->join('user_followers as target_following', 'target_following.user_id', '=', 'viewer_following.user_id')
->where('viewer_following.follower_id', $viewerId)
->where('target_following.follower_id', $targetId)
->count();
$lead = $preview->first();
$label = $count > 1
? sprintf('You both follow %s and %d other%s', $lead->username ?? $lead->name ?? 'someone', $count - 1, $count - 1 === 1 ? '' : 's')
: sprintf('You both follow %s', $lead->username ?? $lead->name ?? 'someone');
return [
'count' => (int) $count,
'label' => $label,
'users' => $this->mapUsers($preview),
];
}
private function mapUsers(Collection $rows): array
{
return $rows->map(fn ($row) => [
'id' => (int) $row->id,
'username' => (string) ($row->username ?? ''),
'name' => (string) ($row->name ?? $row->username ?? ''),
'profile_url' => '/@' . strtolower((string) ($row->username ?? $row->id)),
'avatar_url' => AvatarUrl::forUser((int) $row->id, $row->avatar_hash ?? null, 48),
])->values()->all();
}
}