Repository navigation
feat: let members wait to follow developers who aren't on /dev/null yet #1407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Actions; | ||
|
|
||
| use App\Models\User; | ||
| use Illuminate\Support\Facades\DB; | ||
|
|
||
| final readonly class CreateFollowsFromPendingFollows | ||
| { | ||
| public function __construct(private CreateFollowNotification $createFollowNotification) | ||
| { | ||
| // | ||
| } | ||
|
|
||
| /** | ||
| * The members who waited for a developer to sign up follow them once they do, each with the usual follow | ||
| * notification, which their inbox groups into one. A new user has no followers yet, so none follows twice. | ||
| */ | ||
| public function handle(User $user): void | ||
| { | ||
| $pending = DB::table('pending_follows')->where('github_id', $user->github_id); | ||
| $followers = User::query()->whereIn('id', $pending->clone()->select('user_id'))->get(); | ||
|
|
||
| $user->followers()->attach($followers->modelKeys()); | ||
|
|
||
| foreach ($followers as $follower) { | ||
| $this->createFollowNotification->handle($follower, $user); | ||
| } | ||
|
|
||
| $pending->delete(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Actions; | ||
|
|
||
| use App\Models\Profile; | ||
| use App\Models\User; | ||
| use Illuminate\Support\Facades\DB; | ||
|
|
||
| final readonly class CreatePendingFollow | ||
| { | ||
| /** | ||
| * The user follows the developer once they sign up. Waiting twice is waiting once. The profile is locked, as signing | ||
| * up turns the waits into follows under its lock: a wait for someone who just signed up is dropped rather than left | ||
| * behind. | ||
| */ | ||
| public function handle(User $user, Profile $profile): void | ||
| { | ||
| DB::transaction(function () use ($user, $profile): void { | ||
| if (Profile::query()->lockForUpdate()->find($profile->id)?->isUnclaimed() !== true) { | ||
| return; | ||
| } | ||
|
|
||
| DB::table('pending_follows')->insertOrIgnore(['user_id' => $user->id, 'github_id' => $profile->github_id, 'created_at' => now()]); | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Actions; | ||
|
|
||
| use App\Models\Profile; | ||
| use App\Models\User; | ||
| use Illuminate\Support\Facades\DB; | ||
|
|
||
| final readonly class DeletePendingFollow | ||
| { | ||
| /** | ||
| * The user no longer follows the developer when they sign up, whether they waited from their page or from GitHub. | ||
| */ | ||
| public function handle(User $user, Profile $profile): void | ||
| { | ||
| DB::table('pending_follows')->where('user_id', $user->id)->where('github_id', $profile->github_id)->delete(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Serialize cancellation with profile claim. If a member stops waiting while the developer signs in, conversion can read the pending row before this deletion commits. Conversion can then attach the follow and notify the developer after the member’s cancellation succeeds. Lock the same profile during deletion. If conversion wins the lock, handle the newly created follow instead of deleting zero pending rows. PostgreSQL row locks do not block the conversion’s current plain read. (raw.githubusercontent.com)
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Http\Controllers; | ||
|
|
||
| use App\Actions\CreatePendingFollow; | ||
| use App\Actions\DeletePendingFollow; | ||
| use App\Http\Requests\CreatePendingFollowRequest; | ||
| use App\Http\Requests\DeletePendingFollowRequest; | ||
| use App\Models\Profile; | ||
| use App\Models\User; | ||
| use Illuminate\Container\Attributes\CurrentUser; | ||
| use Illuminate\Http\RedirectResponse; | ||
|
|
||
| /** | ||
| * Following a developer who isn't on /dev/null yet waits for them to sign up. | ||
| */ | ||
| final readonly class PendingFollowController | ||
| { | ||
| public function store(CreatePendingFollowRequest $request, #[CurrentUser] User $user, Profile $profile, CreatePendingFollow $action): RedirectResponse | ||
| { | ||
| $action->handle($user, $profile); | ||
|
|
||
| return back(); | ||
| } | ||
|
|
||
| public function destroy(DeletePendingFollowRequest $request, #[CurrentUser] User $user, Profile $profile, DeletePendingFollow $action): RedirectResponse | ||
| { | ||
| $action->handle($user, $profile); | ||
|
|
||
| return back(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Http\Requests; | ||
|
|
||
| use App\Models\Profile; | ||
| use Illuminate\Auth\Access\Response; | ||
| use Illuminate\Foundation\Http\FormRequest; | ||
|
|
||
| final class CreatePendingFollowRequest extends FormRequest | ||
| { | ||
| /** | ||
| * Only a developer who isn't on /dev/null yet is waited for: once they are, members follow them. | ||
| */ | ||
| public function authorize(): Response | ||
| { | ||
| $profile = $this->route('profile'); | ||
| assert($profile instanceof Profile); | ||
|
|
||
| return $profile->isUnclaimed() ? Response::allow() : Response::denyAsNotFound(); | ||
| } | ||
|
|
||
| /** | ||
| * @return array{} | ||
| */ | ||
| public function rules(): array | ||
| { | ||
| return []; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Http\Requests; | ||
|
|
||
| use App\Models\Profile; | ||
| use Illuminate\Auth\Access\Response; | ||
| use Illuminate\Foundation\Http\FormRequest; | ||
|
|
||
| final class DeletePendingFollowRequest extends FormRequest | ||
| { | ||
| /** | ||
| * Only a developer who isn't on /dev/null yet is waited for: once they are, members follow them. | ||
| */ | ||
| public function authorize(): Response | ||
| { | ||
| $profile = $this->route('profile'); | ||
| assert($profile instanceof Profile); | ||
|
|
||
| return $profile->isUnclaimed() ? Response::allow() : Response::denyAsNotFound(); | ||
| } | ||
|
|
||
| /** | ||
| * @return array{} | ||
| */ | ||
| public function rules(): array | ||
| { | ||
| return []; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.