Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ A signed request sees what the web shows you, such as your own pending commits.
- commit, reply, and quote, with images, and delete your commits;
- preview commit drafts without publishing them;
- follow or unfollow, mute or unmute, and block or unblock, and list who you muted and blocked;
- read who to follow, also as it is suggested beside a profile or a commit, and mark a suggestion not interested;
- star or unstar;
- create polls on commits that are neither replies nor quotes, and vote on polls;
- change Remotes settings, read and send messages with images and link cards in conversations and orgs, mark them pulled, react to messages and list your reactions, find people, and start orgs;
Expand Down
33 changes: 33 additions & 0 deletions app/Http/Controllers/Api/SuggestedUserController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Requests\ListApiSuggestedUsersRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use App\Queries\SuggestedUsersQuery;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

final readonly class SuggestedUserController
{
/**
* Who to follow, as the web suggests them: three people to show and three spares to fill the place of one
* dismissed, each with why they are suggested and whether they follow the user. Each round is the next page of
* the day's picks, as the web's refresh button shows. Beside a profile or a commit, the people that developer
* follows or the thread's participants come first, as the web suggests them there. Nobody suggested is followed
* yet, which each one says, as in any other list of people.
*/
public function index(ListApiSuggestedUsersRequest $request, #[CurrentUser] User $user, SuggestedUsersQuery $suggested): AnonymousResourceCollection
{
$suggestions = $suggested->get($user, round: $request->integer('round'), spares: 3, context: $request->context());

$suggestions->each(function (User $suggestion): void {
$suggestion->setAttribute('is_followed', false);
});

return UserResource::collection($suggestions);
}
}
24 changes: 24 additions & 0 deletions app/Http/Controllers/Api/SuggestionDismissalController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Actions\CreateSuggestionDismissal;
use App\Http\Requests\CreateApiSuggestionDismissalRequest;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\Response;

final readonly class SuggestionDismissalController
{
/**
* Not interested, as on the web: they are never suggested again, and nothing else changes.
*/
public function store(CreateApiSuggestionDismissalRequest $request, #[CurrentUser] User $currentUser, User $user, CreateSuggestionDismissal $action): Response
{
$action->handle($currentUser, $user);

return response()->noContent();
}
}
33 changes: 33 additions & 0 deletions app/Http/Requests/CreateApiSuggestionDismissalRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests;

use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;

final class CreateApiSuggestionDismissalRequest extends FormRequest
{
/**
* Anyone but themselves, as on the web.
*/
public function authorize(): bool
{
$user = $this->user();
assert($user instanceof User);

$dismissed = $this->route('user');
assert($dismissed instanceof User);

return $user->isNot($dismissed);
}

/**
* @return array{}
*/
public function rules(): array
{
return [];
}
}
45 changes: 45 additions & 0 deletions app/Http/Requests/ListApiSuggestedUsersRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests;

use App\Models\Commit;
use App\Models\User;
use App\Rules\VisibleCommit;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;

/**
* Who to follow may be asked for the page it shows on, as the web asks for it beside a profile or a commit. The commit
* has to be one the user can see, as its page requires.
*/
final class ListApiSuggestedUsersRequest extends FormRequest
{
/**
* @return array<string, list<string|ValidationRule>>
*/
public function rules(): array
{
$user = $this->user();
assert($user instanceof User);

return [
'round' => ['nullable', 'integer', 'min:0'],
'username' => ['nullable', 'string', 'exists:users,username'],
'commit_id' => ['bail', 'nullable', 'uuid', 'prohibits:username', new VisibleCommit($user, __('That commit is no longer available.'))],
];
}

public function context(): User|Commit|null
{
$user = $this->user();
assert($user instanceof User);

return match (true) {
$this->filled('username') => User::query()->where('username', $this->string('username')->value())->firstOrFail(),
$this->filled('commit_id') => Commit::query()->visibleTo($user)->findOrFail($this->string('commit_id')->value()),
default => null,
};
}
}
2 changes: 2 additions & 0 deletions app/Http/Resources/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
/**
* A user's public profile, with their counts when they were loaded. A signed request also gets the lane, the color
* the web draws them in, and in a list of people, whether its user follows each one and whether each one follows them.
* Who to follow also says how many people the user follows follow each one.
*
* @mixin User
*/
Expand Down Expand Up @@ -40,6 +41,7 @@ public function toArray(Request $request): array
'lane' => $this->when($request->user() instanceof User, fn (): int|string => $this->lane()),
'is_followed' => $this->whenHas('is_followed'),
'follows_you' => $this->whenHas('follows_you'),
'followed_by_following_count' => $this->whenHas('followed_by_following_count'),
];
}
}
6 changes: 6 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
use App\Http\Controllers\Api\OrgTypingController;
use App\Http\Controllers\Api\RemoteCandidateController;
use App\Http\Controllers\Api\StarredCommitController;
use App\Http\Controllers\Api\SuggestedUserController;
use App\Http\Controllers\Api\SuggestionDismissalController;
use App\Http\Controllers\Api\UserBlockController;
use App\Http\Controllers\Api\UserCommitController;
use App\Http\Controllers\Api\UserController;
Expand Down Expand Up @@ -167,6 +169,10 @@
->middleware('throttle:follows')
->name('api-user-follow.destroy');

// Who to Follow...
Route::get('me/suggested-users', [SuggestedUserController::class, 'index'])->name('api-suggested-user.index');
Route::post('users/{user:username}/dismiss', [SuggestionDismissalController::class, 'store'])->name('api-suggestion-dismissal.store');

// User Mute...
Route::get('me/muted', [UserMuteController::class, 'index'])->name('api-user-mute.index');
Route::post('users/{user:username}/mute', [UserMuteController::class, 'store'])->name('api-user-mute.store');
Expand Down
Loading