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
  • -
    +
    - + -
    +
    + @can(App\Policies\UserPolicy::BLOCK, $user) + @if (Auth::user()->hasBlocked($user)) + +

    Unblocking this user will allow them to mention you again in threads and replies.

    +
    + @else + +

    Blocking this user will prevent them from mentioning you in threads and replies. The user will not be notified that you blocked them.

    +
    + @endif + @endcan + @can(App\Policies\UserPolicy::BAN, $user) @if ($user->isBanned()) +
    +
    +
    +

    + Blocked Users +

    +

    + 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. +

    +
    + +
      + @forelse (Auth::user()->blockedUsers as $user) +
    • + + + + + +
    • + @empty +

      + Currently, you've not blocked anyone. +

      + @endforelse +
    +
    +
    + diff --git a/resources/views/users/settings/password.blade.php b/resources/views/users/settings/password.blade.php index adc2aae74..538960ab7 100644 --- a/resources/views/users/settings/password.blade.php +++ b/resources/views/users/settings/password.blade.php @@ -6,7 +6,7 @@

    - Password Settings + Password

    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 @@

    -

    Danger Zone

    +

    + Danger Zone +

    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') -
    -
    -
    +
    +
    +
    + + +
    +
    @include('users.settings.profile') @include('users.settings.password') @include('users.settings.api_tokens') + @include('users.settings.blocked') @include('users.settings.remove')
    diff --git a/routes/web.php b/routes/web.php index 5252014a8..b7f84e859 100644 --- a/routes/web.php +++ b/routes/web.php @@ -12,6 +12,7 @@ use App\Http\Controllers\Auth\VerificationController; use App\Http\Controllers\Forum\TagsController; use App\Http\Controllers\Forum\ThreadsController; +use App\Http\Controllers\BlockUserController; use App\Http\Controllers\HomeController; use App\Http\Controllers\MarkNotificationsController; use App\Http\Controllers\ProfileController; @@ -20,8 +21,10 @@ use App\Http\Controllers\Settings\ApiTokenController; use App\Http\Controllers\Settings\PasswordController; use App\Http\Controllers\Settings\ProfileController as ProfileSettingsController; +use App\Http\Controllers\Settings\UnblockUserController as UnblockUserSettingsController; use App\Http\Controllers\SocialImageController; use App\Http\Controllers\SubscriptionController; +use App\Http\Controllers\UnblockUserController; use App\Http\Middleware\Authenticate; use Illuminate\Support\Facades\Route; @@ -63,6 +66,8 @@ // Users Route::redirect('/dashboard', '/user'); Route::get('user/{username?}', [ProfileController::class, 'show'])->name('profile'); +Route::put('users/{username}/block', BlockUserController::class)->name('users.block'); +Route::put('users/{username}/unblockblock', UnblockUserController::class)->name('users.unblock'); // Notifications Route::view('notifications', 'users.notifications')->name('notifications')->middleware(Authenticate::class); @@ -73,6 +78,7 @@ Route::put('settings', [ProfileSettingsController::class, 'update'])->name('settings.profile.update'); Route::delete('settings', [ProfileSettingsController::class, 'destroy'])->name('settings.profile.delete'); Route::put('settings/password', [PasswordController::class, 'update'])->name('settings.password.update'); +Route::put('settings/users/{username}/unblock', UnblockUserSettingsController::class)->name('settings.users.unblock'); Route::post('settings/api-tokens', [ApiTokenController::class, 'store'])->name('settings.api-tokens.store'); Route::delete('settings/api-tokens', [ApiTokenController::class, 'destroy'])->name('settings.api-tokens.delete');