diff --git a/app/Livewire/Concerns/Followable.php b/app/Livewire/Concerns/Followable.php new file mode 100644 index 000000000..66611cf14 --- /dev/null +++ b/app/Livewire/Concerns/Followable.php @@ -0,0 +1,63 @@ +check()) { + $this->redirectRoute('login', navigate: true); + + return; + } + + $user = type(auth()->user())->as(User::class); + $user->following()->attach($id); + + if ($this->shouldHandleFollowingCount()) { + $this->dispatch('following.updated'); + } + + $this->dispatch('user.followed', id: $id); + } + + /** + * Unfollows the given user. + */ + #[Renderless] + public function unfollow(int $id): void + { + if (! auth()->check()) { + $this->redirectRoute('login', navigate: true); + + return; + } + + $user = type(auth()->user())->as(User::class); + $user->following()->detach($id); + + if ($this->shouldHandleFollowingCount()) { + $this->dispatch('following.updated'); + } + + $this->dispatch('user.unfollowed', id: $id); + } + + /** + * Indicates if the following count should be handled. + */ + protected function shouldHandleFollowingCount(): bool + { + return false; + } +} diff --git a/app/Livewire/Followers/Index.php b/app/Livewire/Followers/Index.php index 6156758ef..78c69311f 100644 --- a/app/Livewire/Followers/Index.php +++ b/app/Livewire/Followers/Index.php @@ -4,6 +4,7 @@ namespace App\Livewire\Followers; +use App\Livewire\Concerns\Followable; use App\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -15,7 +16,7 @@ final class Index extends Component { - use WithoutUrlPagination, WithPagination; + use Followable, WithoutUrlPagination, WithPagination; /** * The component's user ID. @@ -38,13 +39,27 @@ public function render(): View return view('livewire.followers.index', [ 'user' => $user, 'followers' => $this->isOpened ? $user->followers() - ->when(auth()->user()?->isNot($user), function (Builder|BelongsToMany $query): void { + ->unless(auth()->user()?->is($user), function (Builder|BelongsToMany $query): void { $query->withExists([ 'following as is_follower' => function (Builder $query): void { $query->where('user_id', auth()->id()); }, ]); - })->latest('followers.id')->simplePaginate(10) : collect(), + }) + ->withExists([ + 'followers as is_following' => function (Builder $query): void { + $query->where('follower_id', auth()->id()); + }, + ]) + ->latest('followers.id')->simplePaginate(10) : collect(), ]); } + + /** + * Indicates if the following count should be handled. + */ + protected function shouldHandleFollowingCount(): bool + { + return $this->userId === auth()->id(); + } } diff --git a/app/Livewire/Following/Index.php b/app/Livewire/Following/Index.php index 5f383e801..2c1994d7d 100644 --- a/app/Livewire/Following/Index.php +++ b/app/Livewire/Following/Index.php @@ -4,8 +4,10 @@ namespace App\Livewire\Following; +use App\Livewire\Concerns\Followable; use App\Models\User; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\View\View; use Livewire\Attributes\Locked; use Livewire\Component; @@ -14,7 +16,7 @@ final class Index extends Component { - use WithoutUrlPagination, WithPagination; + use Followable, WithoutUrlPagination, WithPagination; /** * The component's user ID. @@ -36,11 +38,28 @@ public function render(): View return view('livewire.following.index', [ 'user' => $user, - 'following' => $this->isOpened ? $user->following()->withExists([ - 'following as is_follower' => function (Builder $query): void { - $query->where('user_id', auth()->id()); - }, - ])->latest('followers.id')->simplePaginate(10) : collect(), + 'following' => $this->isOpened ? $user->following() + ->withExists([ + 'following as is_follower' => function (Builder $query): void { + $query->where('user_id', auth()->id()); + }, + ]) + ->unless(auth()->user()?->is($user), function (Builder|BelongsToMany $query): void { + $query->withExists([ + 'followers as is_following' => function (Builder $query): void { + $query->where('follower_id', auth()->id()); + }, + ]); + }) + ->latest('followers.id')->simplePaginate(10) : collect(), ]); } + + /** + * Indicates if the following count should be handled. + */ + protected function shouldHandleFollowingCount(): bool + { + return $this->userId === auth()->id(); + } } diff --git a/app/Livewire/Home/Users.php b/app/Livewire/Home/Users.php index 456d2dee1..8d585a24f 100644 --- a/app/Livewire/Home/Users.php +++ b/app/Livewire/Home/Users.php @@ -4,6 +4,7 @@ namespace App\Livewire\Home; +use App\Livewire\Concerns\Followable; use App\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; @@ -14,6 +15,8 @@ final class Users extends Component { + use Followable; + /** * The component's search query. */ @@ -51,6 +54,16 @@ private function usersByQuery(): Collection $query->whereNotNull('answer'); }]) ->orderBy('answered_questions_count', 'desc') + ->when(auth()->check(), function (Builder $query): void { + $query->withExists([ + 'following as is_follower' => function (Builder $query): void { + $query->where('user_id', auth()->id()); + }, + 'followers as is_following' => function (Builder $query): void { + $query->where('follower_id', auth()->id()); + }, + ]); + }) ->limit(10) ->get(); } @@ -67,7 +80,17 @@ private function defaultUsers(): Collection return $this->famousUsers($verifiedUsers) ->merge($verifiedUsers) ->shuffle() - ->load('links'); + ->load('links') + ->when(auth()->check(), function (Collection $users): void { + $users->loadExists([ // @phpstan-ignore-line + 'following as is_follower' => function (Builder $query): void { + $query->where('user_id', auth()->id()); + }, + 'followers as is_following' => function (Builder $query): void { + $query->where('follower_id', auth()->id()); + }, + ]); + }); } /** diff --git a/app/Livewire/Links/Index.php b/app/Livewire/Links/Index.php index 1f3fe74a8..ca35ba253 100644 --- a/app/Livewire/Links/Index.php +++ b/app/Livewire/Links/Index.php @@ -160,6 +160,7 @@ public function unfollow(int $targetId): void #[On('link.created')] #[On('link.updated')] #[On('link-settings.updated')] + #[On('following.updated')] public function refresh(): void { // diff --git a/resources/js/app.js b/resources/js/app.js index c2b0adb7f..061058f95 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -60,6 +60,8 @@ Alpine.data('likeButton', likeButton); import { bookmarkButton } from './bookmark-button.js'; Alpine.data('bookmarkButton', bookmarkButton); +import { followButton } from './follow-button.js' +Alpine.data('followButton', followButton) import { viewCreate } from './view-cerate.js'; Alpine.data('viewCreate', viewCreate); diff --git a/resources/js/follow-button.js b/resources/js/follow-button.js new file mode 100644 index 000000000..e4e1ed716 --- /dev/null +++ b/resources/js/follow-button.js @@ -0,0 +1,50 @@ +const followButton = (id, isFollowing, isFollower, isAuthenticated) => ({ + id, + isFollowing, + isFollower, + isAuthenticated, + buttonText: '', + + init() { + this.setButtonText(); + this.initEventListeners(); + }, + + toggleFollow() { + if (!this.isAuthenticated) { + window.Livewire.navigate('/login'); + return; + } + + if (this.isFollowing) { + this.$wire.unfollow(id); + this.$dispatch('user.unfollowed', { id: id }); + } else { + this.$wire.follow(id); + this.$dispatch('user.followed', { id: id }); + } + }, + + setButtonText() { + this.buttonText = this.isFollowing ? 'Unfollow' : (this.isFollower ? 'Follow Back' : 'Follow'); + }, + + initEventListeners() { + window.addEventListener('user.followed', (event) => { + if (event.detail.id == this.id) { + this.isFollowing = true; + this.setButtonText(); + } + }); + window.addEventListener('user.unfollowed', (event) => { + if (event.detail.id == id) { + this.isFollowing = false; + this.setButtonText(); + } + }); + } +}); + +export { followButton } + + diff --git a/resources/views/components/follow-button.blade.php b/resources/views/components/follow-button.blade.php new file mode 100644 index 000000000..79a695148 --- /dev/null +++ b/resources/views/components/follow-button.blade.php @@ -0,0 +1,21 @@ +@props([ + 'id', + 'isFollower' => false, + 'isFollowing' => false, +]) + +@if(auth()->id() !== $id) +
+
+{{ '@'.$follower->username }} @if (auth()->user()?->isNot($user) && $follower->is_follower) @@ -55,7 +60,13 @@ class="size-4" @endif