diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index a7b1914e5..d974cea0a 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -401,7 +401,7 @@ Sources: [group creation](app/Http/Requests/CreateOrgRequest.php), [invitation e **Mute** someone to remove their commits and replies from your logs, threads, search, and suggestions and stop their notifications. Muting is silent, leaves follows intact, and still lets you visit their profile, which shows a **Muted** badge. Commits you starred stay in Starred. -**Block** someone to stop messaging and interactions such as following, replies, stars, pastéis, and Tom's replies between the two accounts, and hide each other's commits while signed in. Their profile and follower lists become unavailable to you. Blocking removes follows in both directions. Unblocking does not restore those follows. Manage and reverse either choice in Settings → Safety. Blocking is an account-level interaction control, not a way to make public posts inaccessible to signed-out visitors. +**Block** someone to stop messaging and interactions such as following, replies, stars, pastéis, and Tom's replies between the two accounts, and hide each other's commits while signed in. Their profile and follower lists become unavailable to you. Blocking removes follows and stars in both directions, with the notifications for those stars. Unblocking does not restore them. Manage and reverse either choice in Settings → Safety. Blocking is an account-level interaction control, not a way to make public posts inaccessible to signed-out visitors. Sources: [safety settings](resources/js/pages/safety/edit.tsx), [mute action](app/Actions/CreateUserMute.php), [block action](app/Actions/CreateUserBlock.php), [public routes](routes/web.php). diff --git a/app/Actions/CreateUserBlock.php b/app/Actions/CreateUserBlock.php index f05988c69..46da37364 100644 --- a/app/Actions/CreateUserBlock.php +++ b/app/Actions/CreateUserBlock.php @@ -4,13 +4,21 @@ namespace App\Actions; +use App\Enums\NotificationType; +use App\Models\Commit; +use App\Models\Notification; use App\Models\User; use Illuminate\Support\Facades\DB; final readonly class CreateUserBlock { + public function __construct(private UpdateCommitHotScores $updateCommitHotScores) + { + // + } + /** - * Blocking works both ways, so it also ends the follows in both directions. + * Blocking works both ways, so it also ends the follows and removes the stars in both directions. */ public function handle(User $user, User $blocked): void { @@ -19,6 +27,41 @@ public function handle(User $user, User $blocked): void $user->following()->detach($blocked->id); $blocked->following()->detach($user->id); + + $this->updateCommitHotScores->handle([ + ...$this->deleteStars($blocked, $user), + ...$this->deleteStars($user, $blocked), + ]); }); } + + /** + * Removes the stars the stargazer gave the author's commits, with their notifications, so none of them come back + * after an unblock. + * + * @return array The ids of the commits that lost a star. + */ + private function deleteStars(User $stargazer, User $author): array + { + $commitIds = $stargazer->starredCommits() + ->withTrashed() + ->where('commits.user_id', $author->id) + ->get(['commits.id']) + ->map(fn (Commit $commit): string => $commit->id) + ->all(); + + if ($commitIds === []) { + return []; + } + + $stargazer->starredCommits()->detach($commitIds); + + Notification::query() + ->where('type', NotificationType::Starred) + ->where('actor_id', $stargazer->id) + ->whereIn('commit_id', $commitIds) + ->delete(); + + return $commitIds; + } } diff --git a/tests/Unit/Actions/CreateUserBlockTest.php b/tests/Unit/Actions/CreateUserBlockTest.php index 5ab4120d7..7972be856 100644 --- a/tests/Unit/Actions/CreateUserBlockTest.php +++ b/tests/Unit/Actions/CreateUserBlockTest.php @@ -2,8 +2,12 @@ declare(strict_types=1); +use App\Actions\CreateCommitStar; use App\Actions\CreateUserBlock; +use App\Dto\ThreadEngagement; +use App\Models\Commit; use App\Models\User; +use App\Services\TheAlgorithm; it('may block a user, removing follows in both directions', function (): void { $user = User::factory()->create(); @@ -34,3 +38,56 @@ expect($user->blocking()->count())->toBeOne(); }); + +it('removes the stars in both directions, with their notifications', function (): void { + $user = User::factory()->create(); + $blocked = User::factory()->create(); + $friend = User::factory()->create(); + $userCommit = Commit::factory()->for($user)->create(); + $blockedCommit = Commit::factory()->for($blocked)->create(); + $friendCommit = Commit::factory()->for($friend)->create(); + + resolve(CreateCommitStar::class)->handle($blocked, $userCommit); + resolve(CreateCommitStar::class)->handle($user, $blockedCommit); + resolve(CreateCommitStar::class)->handle($friend, $userCommit); + resolve(CreateCommitStar::class)->handle($user, $friendCommit); + resolve(CreateCommitStar::class)->handle($blocked, $friendCommit); + + resolve(CreateUserBlock::class)->handle($user, $blocked); + + expect($blocked->hasStarred($userCommit))->toBeFalse() + ->and($user->hasStarred($blockedCommit))->toBeFalse() + ->and($friend->hasStarred($userCommit))->toBeTrue() + ->and($user->hasStarred($friendCommit))->toBeTrue() + ->and($blocked->hasStarred($friendCommit))->toBeTrue() + ->and($user->notifications()->sole()->actor_id)->toBe($friend->id) + ->and($blocked->notifications()->count())->toBe(0) + ->and($friend->notifications()->count())->toBe(2); +}); + +it('removes the stars on deleted commits too', function (): void { + $user = User::factory()->create(); + $blocked = User::factory()->create(); + $commit = Commit::factory()->for($user)->create(); + + resolve(CreateCommitStar::class)->handle($blocked, $commit); + $commit->delete(); + + resolve(CreateUserBlock::class)->handle($user, $blocked); + + expect($blocked->starredCommits()->withTrashed()->count())->toBe(0) + ->and($user->notifications()->count())->toBe(0); +}); + +it('lowers the commits that lost a star back', function (): void { + $user = User::factory()->create(); + $blocked = User::factory()->create(); + $commit = Commit::factory()->for($user)->create(); + + resolve(CreateCommitStar::class)->handle($blocked, $commit); + + resolve(CreateUserBlock::class)->handle($user, $blocked); + + expect($commit->fresh()->hot_score) + ->toEqualWithDelta(resolve(TheAlgorithm::class)->score($commit->created_at, ThreadEngagement::none()), 1e-9); +});