diff --git a/app/Http/Controllers/API/v1/FollowController.php b/app/Http/Controllers/API/v1/FollowController.php new file mode 100644 index 000000000..e60809cdf --- /dev/null +++ b/app/Http/Controllers/API/v1/FollowController.php @@ -0,0 +1,127 @@ +validate(['userId' => ['required', 'exists:users,id']]); + $userToFollow = User::find($validated['userId']); + + try { + $createFollowResponse = UserBackend::createOrRequestFollow(Auth::user(), $userToFollow); + } catch (AlreadyFollowingException) { + return $instance->sendv1Error(['message' => __('controller.user.follow-error')], 409); + } catch (IdenticalModelException) { + abort(409); + } + + return $instance->sendv1Response(new UserResource($createFollowResponse), 204); + } + + public static function destroyFollow(Request $request, FollowController $instance): JsonResponse { + $validated = $request->validate(['userId' => ['required', 'exists:users,id']]); + $userToUnfollow = User::find($validated['userId']); + + $destroyFollowResponse = UserBackend::destroyFollow(Auth::user(), $userToUnfollow); + if ($destroyFollowResponse === false) { + return $instance->sendv1Error(['message' => __('controller.user.follow-404')], 409); + } + + $userToUnfollow->fresh(); + return $instance->sendv1Response(new UserResource($userToUnfollow)); + + } + + public function getFollowers(): AnonymousResourceCollection { + $followersResponse = FollowBackend::getFollowers(user: auth()->user()); + return UserResource::collection($followersResponse); + } + + public function getFollowRequests(): AnonymousResourceCollection { + $followRequestResponse = FollowBackend::getFollowRequests(user: auth()->user()); + return UserResource::collection($followRequestResponse); + } + + public function getFollowings(): AnonymousResourceCollection { + $followingResponse = FollowBackend::getFollowings(user: auth()->user()); + return UserResource::collection($followingResponse); + } + + public function removeFollower(Request $request): void { + $validated = $request->validate([ + 'userId' => [ + 'required', + Rule::in(auth()->user()->followers->pluck('user_id')), + ] + ]); + + $follow = Follow::where('user_id', $validated['userId']) + ->where('follow_id', auth()->user()->id) + ->firstOrFail(); + + try { + $removeResponse = FollowBackend::removeFollower(follow: $follow, user: auth()->user()); + } catch (PermissionException) { + abort(403); + } + + if ($removeResponse === true) { + abort(204); + } + abort(500); + } + + public function approveFollowRequest(Request $request) { + $validated = $request->validate([ + 'userId' => [ + 'required', + Rule::in(auth()->user()->followRequests->pluck('user_id')) + ] + ]); + + try { + FollowBackend::approveFollower(auth()->user()->id, $validated['userId']); + abort(204); + } catch (ModelNotFoundException) { + abort(404); + } catch (AlreadyFollowingException $exception) { + report($exception); + } + abort(500); + } + + public function rejectFollowRequest(Request $request) { + $validated = $request->validate([ + 'userId' => [ + 'required', + Rule::in(auth()->user()->followRequests->pluck('user_id')) + ] + ]); + try { + FollowBackend::rejectFollower(auth()->user()->id, $validated['userId']); + abort(204); + } catch (ModelNotFoundException) { + abort(404); + } + abort(500); + } +} diff --git a/app/Http/Controllers/API/v1/UserController.php b/app/Http/Controllers/API/v1/UserController.php index 9ccca674a..fb7ab1de9 100644 --- a/app/Http/Controllers/API/v1/UserController.php +++ b/app/Http/Controllers/API/v1/UserController.php @@ -3,8 +3,6 @@ namespace App\Http\Controllers\API\v1; -use App\Exceptions\AlreadyFollowingException; -use App\Exceptions\IdenticalModelException; use App\Exceptions\PermissionException; use App\Exceptions\UserAlreadyMutedException; use App\Exceptions\UserNotMutedException; @@ -68,35 +66,6 @@ public function show(string $username): UserResource { return new UserResource(User::where('username', 'like', $username)->firstOrFail()); } - public function createFollow(Request $request): JsonResponse { - $validated = $request->validate(['userId' => ['required', 'exists:users,id']]); - $userToFollow = User::find($validated['userId']); - - try { - $createFollowResponse = UserBackend::createOrRequestFollow(Auth::user(), $userToFollow); - } catch (AlreadyFollowingException) { - return $this->sendv1Error(['message' => __('controller.user.follow-error')], 409); - } catch (IdenticalModelException) { - abort(409); - } - - return $this->sendv1Response(new UserResource($createFollowResponse), 201); - } - - public function destroyFollow(Request $request): JsonResponse { - $validated = $request->validate(['userId' => ['required', 'exists:users,id']]); - $userToUnfollow = User::find($validated['userId']); - - $destroyFollowResponse = UserBackend::destroyFollow(Auth::user(), $userToUnfollow); - if ($destroyFollowResponse === false) { - return $this->sendv1Error(['message' => __('controller.user.follow-404')], 409); - } - - $userToUnfollow->fresh(); - return $this->sendv1Response(new UserResource($userToUnfollow)); - - } - public function createMute(Request $request): JsonResponse { $validated = $request->validate([ 'userId' => [ diff --git a/app/Http/Controllers/Backend/User/FollowController.php b/app/Http/Controllers/Backend/User/FollowController.php new file mode 100644 index 000000000..5f2dc84e9 --- /dev/null +++ b/app/Http/Controllers/Backend/User/FollowController.php @@ -0,0 +1,74 @@ +userFollowers()->simplePaginate(perPage: 15); + } + + public static function getFollowRequests(User $user): Paginator { + return $user->userFollowRequests()->simplePaginate(perPage: 15); + } + + public static function getFollowings(User $user): Paginator { + return $user->userFollowings()->simplePaginate(perPage: 15); + } + + /** + * @param Follow $follow + * @param User $user - The acting user + * + * @return bool|null + * @throws PermissionException + */ + public static function removeFollower(Follow $follow, User $user): bool|null { + if ($user->cannot('delete', $follow)) { + throw new PermissionException(); + } + return $follow->delete(); + } + + /** + * @param int $userId + * @param int $followerID + * + * @return FollowRequest|null + */ + public static function rejectFollower(int $userId, int $followerID): ?FollowRequest { + $request = FollowRequest::where('user_id', $followerID)->where('follow_id', $userId)->firstOrFail(); + + $request->delete(); + return $request; + } + + /** + * + * @param int $userId The id of the user who is approving a follower + * @param int $approverId The id of a to-be-approved follower + * + * @throws ModelNotFoundException|AlreadyFollowingException + */ + public static function approveFollower(int $userId, int $approverId): bool { + $request = FollowRequest::where('user_id', $approverId)->where('follow_id', $userId)->firstOrFail(); + + $follow = UserController::createFollow($request->user, $request->requestedFollow, true); + + if ($follow) { + $request->delete(); + } + return $follow; + } + +} diff --git a/app/Http/Controllers/Frontend/SettingsController.php b/app/Http/Controllers/Frontend/SettingsController.php index ba01c0d83..0481317f8 100644 --- a/app/Http/Controllers/Frontend/SettingsController.php +++ b/app/Http/Controllers/Frontend/SettingsController.php @@ -4,10 +4,11 @@ use App\Enum\StatusVisibility; use App\Exceptions\AlreadyFollowingException; +use App\Http\Controllers\Backend\User\FollowController; +use App\Http\Controllers\Backend\User\FollowController as SettingsBackend; use App\Http\Controllers\Backend\User\SessionController; use App\Http\Controllers\Backend\User\TokenController; use App\Http\Controllers\Controller; -use App\Http\Controllers\SettingsController as SettingsBackend; use Illuminate\Contracts\Support\Renderable; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\RedirectResponse; @@ -138,7 +139,7 @@ public function rejectFollower(Request $request): RedirectResponse { ] ]); try { - $approval = SettingsBackend::rejectFollower(auth()->user()->id, $validated['user_id']); + $approval = FollowController::rejectFollower(auth()->user()->id, $validated['user_id']); } catch (ModelNotFoundException) { abort(404); } diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 2b3b366d6..5b4c455af 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -2,11 +2,8 @@ namespace App\Http\Controllers; -use App\Exceptions\AlreadyFollowingException; use App\Models\Follow; -use App\Models\FollowRequest; use Illuminate\Contracts\Support\Renderable; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Validation\Rule; @@ -16,6 +13,9 @@ */ class SettingsController extends Controller { + /** + * @deprecated + */ public function renderFollowerSettings(): Renderable { return view('settings.follower', [ 'requests' => auth()->user()->followRequests()->with('user')->paginate(15), @@ -23,6 +23,9 @@ public function renderFollowerSettings(): Renderable { ]); } + /** + * @deprecated + */ public function removeFollower(Request $request): RedirectResponse { $validated = $request->validate([ 'user_id' => [ @@ -40,36 +43,4 @@ public function removeFollower(Request $request): RedirectResponse { return back()->with('success', __('settings.follower.delete-success')); } - - /** - * - * @param int $userId The id of the user who is approving a follower - * @param int $approverId The id of a to-be-approved follower - * - * @throws ModelNotFoundException|AlreadyFollowingException - */ - public static function approveFollower(int $userId, int $approverId): bool { - $request = FollowRequest::where('user_id', $approverId)->where('follow_id', $userId)->firstOrFail(); - - $follow = UserController::createFollow($request->user, $request->requestedFollow, true); - - if ($follow) { - $request->delete(); - } - return $follow; - } - - /** - * @param int $userId - * @param int $followerID - * - * @return FollowRequest|null - */ - public static function rejectFollower(int $userId, int $followerID): ?FollowRequest { - $request = FollowRequest::where('user_id', $followerID)->where('follow_id', $userId)->firstOrFail(); - - $request->delete(); - return $request; - - } } diff --git a/app/Models/Follow.php b/app/Models/Follow.php index 94898d0ee..4a7dd8bd0 100644 --- a/app/Models/Follow.php +++ b/app/Models/Follow.php @@ -18,6 +18,10 @@ class Follow extends Model ]; public function user(): BelongsTo { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class, 'user_id', 'id'); + } + + public function following() { + return $this->belongsTo(User::class, 'follow_id', 'id'); } } diff --git a/app/Models/User.php b/app/Models/User.php index 648b5c86d..a07b67a25 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -116,10 +116,20 @@ public function followRequests(): HasMany { return $this->hasMany(FollowRequest::class, 'follow_id', 'id'); } + /** + * @deprecated + */ public function followers(): HasMany { return $this->hasMany(Follow::class, 'follow_id', 'id'); } + /** + * @deprecated + */ + public function followings(): HasMany { + return $this->hasMany(Follow::class, 'user_id', 'id'); + } + public function sessions(): HasMany { return $this->hasMany(Session::class); } @@ -135,6 +145,30 @@ public function getPointsAttribute(): int { ->sum('points'); } + /** + * @untested + * @todo test + */ + public function userFollowings(): BelongsToMany { + return $this->belongsToMany(__CLASS__, 'follows', 'user_id', 'follow_id'); + } + + /** + * @untested + * @todo test + */ + public function userFollowers(): BelongsToMany { + return $this->belongsToMany(__CLASS__, 'follows', 'follow_id', 'user_id'); + } + + /** + * @untested + * @todo test + */ + public function userFollowRequests(): BelongsToMany { + return $this->belongsToMany(__CLASS__, 'follow_requests', 'follow_id', 'user_id'); + } + /** * @deprecated -> replaced by $user->can(...) / $user->cannot(...) / request()->user()->can(...) / * request()->user()->cannot(...) diff --git a/resources/components/FollowTable.vue b/resources/components/FollowTable.vue new file mode 100644 index 000000000..2190c78c8 --- /dev/null +++ b/resources/components/FollowTable.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/resources/components/Settings/ProfileSettings.vue b/resources/components/Settings/ProfileSettings.vue index b86d5aef1..4508f299c 100644 --- a/resources/components/Settings/ProfileSettings.vue +++ b/resources/components/Settings/ProfileSettings.vue @@ -41,7 +41,21 @@ i18n.get('_.settings.title-privacy') }} +
+
+ +
+
+ + {{i18n.get('_.settings.follower.manage')}} + +
+
+ +