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')
}}
+
+
+
+
+
+
+
+
+ @{{ row.username }}
+
+
+
+ {{ i18n.get("_.leaderboard.distance") }}
+ {{ row.trainDistance / 1000 }}
+
+ km
+
+
+
+
+ {{ i18n.get("_.leaderboard.duration") }}
+ {{ row.trainDuration.toFixed(0) }}
+
+
+ min
+
+
+
+
+ {{ i18n.get("_.leaderboard.points") }}
+ {{ row.points }}
+
+ {{ i18n.get("_.profile.points-abbr") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+