diff --git a/app/Http/Controllers/BlockUserController.php b/app/Http/Controllers/BlockUserController.php new file mode 100644 index 000000000..f2ab502b4 --- /dev/null +++ b/app/Http/Controllers/BlockUserController.php @@ -0,0 +1,28 @@ +middleware(Authenticate::class); + } + + public function __invoke(Request $request, User $user) + { + $this->authorize(UserPolicy::BLOCK, $user); + + $this->dispatchSync(new BlockUser($request->user(), $user)); + + $this->success('settings.user.blocked'); + + return redirect()->route('profile', $user->username()); + } +} diff --git a/app/Http/Controllers/Settings/ApiTokenController.php b/app/Http/Controllers/Settings/ApiTokenController.php index 224989b67..22ef58b43 100644 --- a/app/Http/Controllers/Settings/ApiTokenController.php +++ b/app/Http/Controllers/Settings/ApiTokenController.php @@ -8,7 +8,6 @@ use App\Jobs\CreateApiToken; use App\Jobs\DeleteApiToken; use Illuminate\Auth\Middleware\Authenticate; -use Illuminate\Support\Facades\Auth; class ApiTokenController extends Controller { @@ -19,7 +18,7 @@ public function __construct() public function store(CreateApiTokenRequest $request) { - $this->dispatchSync(new CreateApiToken($user = Auth::user(), $request->name())); + $this->dispatchSync(new CreateApiToken($user = $request->user(), $request->name())); $token = $user->tokens()->where('name', $request->name())->first(); @@ -30,7 +29,7 @@ public function store(CreateApiTokenRequest $request) public function destroy(DeleteApiTokenRequest $request) { - $this->dispatchSync(new DeleteApiToken(Auth::user(), $request->id())); + $this->dispatchSync(new DeleteApiToken($request->user(), $request->id())); $this->success('settings.api_token.deleted'); diff --git a/app/Http/Controllers/Settings/PasswordController.php b/app/Http/Controllers/Settings/PasswordController.php index 62c9fe009..d27822005 100644 --- a/app/Http/Controllers/Settings/PasswordController.php +++ b/app/Http/Controllers/Settings/PasswordController.php @@ -6,7 +6,6 @@ use App\Http\Requests\UpdatePasswordRequest; use App\Jobs\UpdatePassword; use Illuminate\Auth\Middleware\Authenticate; -use Illuminate\Support\Facades\Auth; class PasswordController extends Controller { @@ -17,7 +16,7 @@ public function __construct() public function update(UpdatePasswordRequest $request) { - $this->dispatchSync(new UpdatePassword(Auth::user(), $request->newPassword())); + $this->dispatchSync(new UpdatePassword($request->user(), $request->newPassword())); $this->success('settings.password.updated'); diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php index af15a5547..dad816126 100644 --- a/app/Http/Controllers/Settings/ProfileController.php +++ b/app/Http/Controllers/Settings/ProfileController.php @@ -9,7 +9,6 @@ use App\Policies\UserPolicy; use Illuminate\Auth\Middleware\Authenticate; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; class ProfileController extends Controller { @@ -25,7 +24,7 @@ public function edit() public function update(UpdateProfileRequest $request) { - $this->dispatchSync(UpdateProfile::fromRequest(Auth::user(), $request)); + $this->dispatchSync(UpdateProfile::fromRequest($request->user(), $request)); $this->success('settings.updated'); diff --git a/app/Http/Controllers/Settings/UnblockUserController.php b/app/Http/Controllers/Settings/UnblockUserController.php new file mode 100644 index 000000000..a3b83ab6c --- /dev/null +++ b/app/Http/Controllers/Settings/UnblockUserController.php @@ -0,0 +1,29 @@ +middleware(Authenticate::class); + } + + public function __invoke(Request $request, User $user) + { + $this->authorize(UserPolicy::BLOCK, $user); + + $this->dispatchSync(new UnblockUser($request->user(), $user)); + + $this->success('settings.user.unblocked'); + + return redirect()->route('settings.profile'); + } +} diff --git a/app/Http/Controllers/UnblockUserController.php b/app/Http/Controllers/UnblockUserController.php new file mode 100644 index 000000000..04d1ee717 --- /dev/null +++ b/app/Http/Controllers/UnblockUserController.php @@ -0,0 +1,28 @@ +middleware(Authenticate::class); + } + + public function __invoke(Request $request, User $user) + { + $this->authorize(UserPolicy::BLOCK, $user); + + $this->dispatchSync(new UnblockUser($request->user(), $user)); + + $this->success('settings.user.unblocked'); + + return redirect()->route('profile', $user->username()); + } +} diff --git a/app/Jobs/BlockUser.php b/app/Jobs/BlockUser.php new file mode 100644 index 000000000..b9545f171 --- /dev/null +++ b/app/Jobs/BlockUser.php @@ -0,0 +1,17 @@ +user->blockedUsers()->attach($this->blockedUser); + } +} diff --git a/app/Jobs/UnblockUser.php b/app/Jobs/UnblockUser.php new file mode 100644 index 000000000..339ed54c4 --- /dev/null +++ b/app/Jobs/UnblockUser.php @@ -0,0 +1,17 @@ +user->blockedUsers()->detach($this->blockedUser); + } +} diff --git a/app/Listeners/NotifyUsersMentionedInReply.php b/app/Listeners/NotifyUsersMentionedInReply.php index c4647083f..8b62bdedf 100644 --- a/app/Listeners/NotifyUsersMentionedInReply.php +++ b/app/Listeners/NotifyUsersMentionedInReply.php @@ -12,7 +12,9 @@ final class NotifyUsersMentionedInReply public function handle(ReplyWasCreated $event): void { $event->reply->mentionedUsers()->each(function ($user) use ($event) { - $user->notify(new MentionNotification($event->reply)); + if (! $user->hasBlocked($event->reply->author())) { + $user->notify(new MentionNotification($event->reply)); + } }); } } diff --git a/app/Listeners/NotifyUsersMentionedInThread.php b/app/Listeners/NotifyUsersMentionedInThread.php index 25b3c45a8..0924f6ce2 100644 --- a/app/Listeners/NotifyUsersMentionedInThread.php +++ b/app/Listeners/NotifyUsersMentionedInThread.php @@ -12,7 +12,9 @@ final class NotifyUsersMentionedInThread public function handle(ThreadWasCreated $event): void { $event->thread->mentionedUsers()->each(function ($user) use ($event) { - $user->notify(new MentionNotification($event->thread)); + if (! $user->hasBlocked($event->thread->author())) { + $user->notify(new MentionNotification($event->thread)); + } }); } } diff --git a/app/Models/User.php b/app/Models/User.php index dc427f05b..c0d9470b1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -196,6 +196,11 @@ public function replyAble(): HasMany return $this->hasMany(Reply::class, 'author_id'); } + public function blockedUsers() + { + return $this->belongsToMany(User::class, 'blocked_users', 'user_id', 'blocked_user_id'); + } + public function articles(): HasMany { return $this->hasMany(Article::class, 'author_id'); @@ -301,4 +306,23 @@ public function scopeModerators(Builder $query) self::MODERATOR, ]); } + + public function hasBlocked(User $user): bool + { + return $this->blockedUsers()->where('blocked_user_id', $user->getKey())->exists(); + } + + public function scopeWithUsersWhoDoesntBlock(Builder $query, User $user) + { + return $query->whereDoesntHave('blockedUsers', function ($query) use ($user) { + $query->where('blocked_user_id', $user->getKey()); + }); + } + + public function scopeWithUsersWhoArentBlockedBy(Builder $query, User $user) + { + return $query->whereDoesntHave('blockedUsers', function ($query) use ($user) { + $query->where('user_id', $user->getKey()); + }); + } } diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php index 3f9268cb3..519a218ec 100644 --- a/app/Policies/UserPolicy.php +++ b/app/Policies/UserPolicy.php @@ -10,6 +10,8 @@ final class UserPolicy const BAN = 'ban'; + const BLOCK = 'block'; + const DELETE = 'delete'; public function admin(User $user): bool @@ -23,6 +25,11 @@ public function ban(User $user, User $subject): bool ($user->isModerator() && ! $subject->isAdmin() && ! $subject->isModerator()); } + public function block(User $user, User $subject): bool + { + return ! $user->is($subject) && ! $subject->isModerator() && ! $subject->isAdmin(); + } + public function delete(User $user, User $subject): bool { return ($user->isAdmin() || $user->is($subject)) && ! $subject->isAdmin(); diff --git a/database/migrations/2022_06_14_072001_create_blocked_users_table.php b/database/migrations/2022_06_14_072001_create_blocked_users_table.php new file mode 100644 index 000000000..3ff2aba3e --- /dev/null +++ b/database/migrations/2022_06_14_072001_create_blocked_users_table.php @@ -0,0 +1,19 @@ +id(); + $table->unsignedInteger('user_id'); + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); + $table->unsignedInteger('blocked_user_id'); + $table->foreign('blocked_user_id')->references('id')->on('users')->cascadeOnDelete(); + }); + } +}; diff --git a/database/schema/mysql-schema.dump b/database/schema/mysql-schema.dump index 23d459a9d..d52e66bad 100644 --- a/database/schema/mysql-schema.dump +++ b/database/schema/mysql-schema.dump @@ -31,6 +31,20 @@ CREATE TABLE `articles` ( CONSTRAINT `articles_author_id_foreign` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `blocked_users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `blocked_users` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `blocked_user_id` int unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `blocked_users_user_id_foreign` (`user_id`), + KEY `blocked_users_blocked_user_id_foreign` (`blocked_user_id`), + CONSTRAINT `blocked_users_blocked_user_id_foreign` FOREIGN KEY (`blocked_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `blocked_users_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `failed_jobs`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; @@ -355,3 +369,4 @@ INSERT INTO `migrations` VALUES (67,'2021_10_12_170118_add_locked_by_column_to_t INSERT INTO `migrations` VALUES (68,'2019_12_14_000001_create_personal_access_tokens_table',6); INSERT INTO `migrations` VALUES (69,'2022_04_06_152416_add_uuids_to_tables',7); INSERT INTO `migrations` VALUES (70,'2022_05_10_180922_make_uuids_non_nullable',7); +INSERT INTO `migrations` VALUES (71,'2022_06_14_072001_create_blocked_users_table',8); diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index bbe8542c3..cd0e921fa 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -13,7 +13,7 @@ class UserSeeder extends Seeder { public function run() { - User::factory()->createQuietly([ + $admin = User::factory()->createQuietly([ 'name' => 'Test User', 'email' => 'test@example.com', 'username' => 'testing', @@ -59,5 +59,8 @@ public function run() ->inRandomOrder() ->take(4) ->update(['is_pinned' => true]); + + // Block some users... + $admin->blockedUsers()->sync(range(20, 24)); } } diff --git a/lang/en/settings.php b/lang/en/settings.php index 63aaedad1..f4b8fab69 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -5,6 +5,8 @@ 'updated' => 'Settings successfully saved! If you changed your email address you\'ll receive an email address to re-confirm it.', 'deleted' => 'Account was successfully removed.', 'password.updated' => 'Password successfully changed!', + 'user.blocked' => 'User successfully blocked.', + 'user.unblocked' => 'User successfully unblocked.', 'api_token' => [ 'created' => 'API token created! Please copy the following token as it will not be shown again:', 'deleted' => 'API token successfully removed.', diff --git a/resources/views/emails/mention.blade.php b/resources/views/emails/mention.blade.php index d7ed5fb5e..428f1b351 100644 --- a/resources/views/emails/mention.blade.php +++ b/resources/views/emails/mention.blade.php @@ -10,4 +10,9 @@ View Thread @endcomponent +@component('mail::subcopy') +If you do not want this user to be able to mention you anymore, you may +[block them through their profile]({{ route('profile', $mentionAble->author()->username()) }}). +@endcomponent + @endcomponent diff --git a/resources/views/layouts/_nav.blade.php b/resources/views/layouts/_nav.blade.php index 002e361ff..489a0fad9 100644 --- a/resources/views/layouts/_nav.blade.php +++ b/resources/views/layouts/_nav.blade.php @@ -184,7 +184,7 @@ @else
Unblocking this user will allow them to mention you again in threads and replies.
+Blocking this user will prevent them from mentioning you in threads and replies. The user will not be notified that you blocked them.
++ The users below will not be able to mention you in their forum threads or replies. You can block additional users from their profile. Or you can unblock users below. +
++ Currently, you've not blocked anyone. +
+ @endforelse +Update the password used for logging into your account. diff --git a/resources/views/users/settings/remove.blade.php b/resources/views/users/settings/remove.blade.php index 2a1d98274..038dd57f5 100644 --- a/resources/views/users/settings/remove.blade.php +++ b/resources/views/users/settings/remove.blade.php @@ -3,7 +3,9 @@
Please be aware that deleting your account will also remove all of your data, including your threads and replies. This cannot be undone.
@@ -27,4 +29,4 @@ >Deleting your account will remove any related content like threads & replies. This cannot be undone.
-@endunless \ No newline at end of file +@endunless diff --git a/resources/views/users/settings/settings.blade.php b/resources/views/users/settings/settings.blade.php index 3f059e25a..3bdc999c5 100644 --- a/resources/views/users/settings/settings.blade.php +++ b/resources/views/users/settings/settings.blade.php @@ -12,12 +12,41 @@ @include('layouts._alerts') -