optimizations
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user