From b8fd94312caeaca211dc6966a087619b3e513079 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 22 Nov 2023 09:33:52 -0700 Subject: [PATCH 01/47] add migration steps --- ...00000_create_user_message_chains_table.php | 35 +++++++++++++++++ ...1_22_000001_create_user_messages_table.php | 38 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php create mode 100644 database/migrations/platform/2023_11_22_000001_create_user_messages_table.php diff --git a/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php b/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php new file mode 100644 index 0000000000..96026e5e14 --- /dev/null +++ b/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->string('title'); + $table->unsignedBigInteger('sender_id')->nullable(); + $table->unsignedBigInteger('recipient_id')->nullable(); + $table->integer('num_messages')->default(0); + $table->integer('sender_num_unread')->default(0); + $table->integer('recipient_num_unread')->default(0); + $table->timestampTz('sender_deleted_at')->nullable(); + $table->timestampTz('recipient_deleted_at')->nullable(); + + $table->index('sender_id'); + $table->index('recipient_id'); + + $table->foreign('sender_id')->references('ID')->on('UserAccounts')->onDelete('set null'); + $table->foreign('recipient_id')->references('ID')->on('UserAccounts')->onDelete('set null'); + }); + } + + public function down(): void + { + Schema::dropIfExists('user_message_chains'); + } +}; diff --git a/database/migrations/platform/2023_11_22_000001_create_user_messages_table.php b/database/migrations/platform/2023_11_22_000001_create_user_messages_table.php new file mode 100644 index 0000000000..f834440a68 --- /dev/null +++ b/database/migrations/platform/2023_11_22_000001_create_user_messages_table.php @@ -0,0 +1,38 @@ +bigIncrements('id'); + $table->unsignedBigInteger('chain_id'); + $table->unsignedBigInteger('author_id'); + $table->text('body'); + $table->timestampTz('created_at')->useCurrent(); + + $table->index('chain_id'); + + $table->foreign('chain_id')->references('ID')->on('user_message_chains')->onDelete('cascade'); + $table->foreign('author_id')->references('ID')->on('UserAccounts')->onDelete('cascade'); + }); + + Schema::table('Messages', function (Blueprint $table) { + $table->unsignedBigInteger('migrated_id')->nullable(); + }); + } + + public function down(): void + { + Schema::table('Messages', function (Blueprint $table) { + $table->dropColumn('migrated_id'); + }); + + Schema::dropIfExists('user_messages'); + } +}; From 4af4d3c8081bc26962160ee8a27b0779d5902f8e Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 22 Nov 2023 10:50:44 -0700 Subject: [PATCH 02/47] add sync command --- app/Community/Commands/SyncMessages.php | 87 ++++++++++++++++++++--- app/Community/Models/UserMessage.php | 47 ++++++++++++ app/Community/Models/UserMessageChain.php | 55 ++++++++++++++ 3 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 app/Community/Models/UserMessage.php create mode 100644 app/Community/Models/UserMessageChain.php diff --git a/app/Community/Commands/SyncMessages.php b/app/Community/Commands/SyncMessages.php index 0f89c6e0e8..668d78d7be 100644 --- a/app/Community/Commands/SyncMessages.php +++ b/app/Community/Commands/SyncMessages.php @@ -4,16 +4,15 @@ namespace App\Community\Commands; -use App\Support\Sync\SyncTrait; -use Exception; +use App\Community\Models\Message; +use App\Community\Models\UserMessage; +use App\Community\Models\UserMessageChain; +use App\Site\Models\User; use Illuminate\Console\Command; class SyncMessages extends Command { - use SyncTrait; - - protected $signature = 'ra:sync:messages {id?} {--f|full} {--p|no-post}'; - + protected $signature = 'ra:sync:messages'; protected $description = 'Sync messages'; public function __construct() @@ -21,11 +20,79 @@ public function __construct() parent::__construct(); } - /** - * @throws Exception - */ public function handle(): void { - $this->sync('messages'); + $count = Message::whereNull('migrated_id')->count(); + + $progressBar = $this->output->createProgressBar($count); + $progressBar->start(); + + // have to do this in batches to prevent exhausting memory + // due to requesting payloads (message content) + for ($i = 0; $i < $count; $i += 100) { + $messages = Message::whereNull('migrated_id')->orderBy('ID')->limit(100)->get(); + /** @var Message $message */ + foreach ($messages as $message) { + $this->migrateMessage($message); + $progressBar->advance(); + } + } + + $progressBar->finish(); + $this->line(PHP_EOL); + } + + private function migrateMessage(Message $message) + { + $userFrom = User::where('User', $message->UserFrom)->first(); + $userTo = User::where('User', $message->UserTo)->first(); + if (!$userFrom || !$userTo) { + // sender or recipient was deleted. ignore message + $message->migrated_id = 0; + $message->save(); + return; + } + + if (strtolower(substr($message->Title, 0, 4)) == 're: ') { + $parent = Message::where('Title', '=', substr($message->Title, 4)) + ->where('UserFrom', '=', $message->UserTo) + ->where('UserTo', '=', $message->UserFrom) + ->where('ID', '<', $message->ID) + ->first(); + } else { + $parent = null; + } + + if ($parent === null) { + $userMessageChain = new UserMessageChain([ + 'title' => $message->Title, + 'sender_id' => $userFrom->ID, + 'recipient_id' => $userTo->ID, + ]); + } else { + $migratedParent = UserMessage::where('id', $parent->migrated_id)->firstOrFail(); + $userMessageChain = UserMessageChain::where('id', $migratedParent->chain_id)->firstOrFail(); + } + + $userMessageChain->num_messages++; + if ($message->Unread) { + if ($userMessageChain->recipient_id == $userTo->ID) { + $userMessageChain->recipient_num_unread++; + } else { + $userMessageChain->sender_num_unread++; + } + } + $userMessageChain->save(); + + $userMessage = new UserMessage([ + 'chain_id' => $userMessageChain->id, + 'author_id' => $userFrom->ID, + 'body' => $message->Payload, + 'created_at' => $message->TimeSent, + ]); + $userMessage->save(); + + $message->migrated_id = $userMessage->id; + $message->save(); } } diff --git a/app/Community/Models/UserMessage.php b/app/Community/Models/UserMessage.php new file mode 100644 index 0000000000..53abf813fc --- /dev/null +++ b/app/Community/Models/UserMessage.php @@ -0,0 +1,47 @@ + 'datetime', + ]; + + // == accessors + + // == mutators + + // == relations + + /** + * @return BelongsTo + */ + public function author(): BelongsTo + { + return $this->belongsTo(User::class, 'author_id'); + } + + // == scopes +} diff --git a/app/Community/Models/UserMessageChain.php b/app/Community/Models/UserMessageChain.php new file mode 100644 index 0000000000..121d05b3cc --- /dev/null +++ b/app/Community/Models/UserMessageChain.php @@ -0,0 +1,55 @@ + 'datetime', + 'recipient_deleted_at' => 'datetime', + ]; + + // == accessors + + // == mutators + + // == relations + + /** + * @return BelongsTo + */ + public function sender(): BelongsTo + { + return $this->belongsTo(User::class, 'sender_id'); + } + + /** + * @return BelongsTo + */ + public function recipient(): BelongsTo + { + return $this->belongsTo(User::class, 'recipient_id'); + } + + // == scopes +} From 095f543c17b4362d7c22b9e85092b5357b05f9b9 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 22 Nov 2023 13:13:54 -0700 Subject: [PATCH 03/47] add message viewer --- .../UserMessageChainController.php | 55 +++++++++++++++++ app/Community/RouteServiceProvider.php | 11 ++-- .../components/message/breadcrumbs.blade.php | 10 +++ .../message/view-chain-page.blade.php | 61 +++++++++++++++++++ .../paginator.blade.php | 4 ++ .../completion-progress-page.blade.php | 2 +- 6 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 app/Community/Controllers/UserMessageChainController.php create mode 100644 resources/views/community/components/message/breadcrumbs.blade.php create mode 100644 resources/views/community/components/message/view-chain-page.blade.php rename resources/views/{platform/components/completion-progress-page => components}/paginator.blade.php (97%) diff --git a/app/Community/Controllers/UserMessageChainController.php b/app/Community/Controllers/UserMessageChainController.php new file mode 100644 index 0000000000..4d880a6857 --- /dev/null +++ b/app/Community/Controllers/UserMessageChainController.php @@ -0,0 +1,55 @@ +route('chain'); + + $messageChain = UserMessageChain::firstWhere('id', $chainId); + if (!$messageChain) { + abort(404); + } + + $user = $request->user(); + if ($messageChain->sender_id != $user->id && $messageChain->recipient_id != $user->id) { + // TODO: abort(404); + } + + $pageSize = 20; + $currentPage = (int) $request->input('page.number') ?? 1; + if ($currentPage < 1) { + $currentPage = 1; + } + $totalPages = (int) (($messageChain->num_messages + 19) / 20); + + $messages = UserMessage::where('chain_id', $chainId) + ->offset(($currentPage - 1) * $pageSize) + ->limit($pageSize) + ->get(); + + return view('community.components.message.view-chain-page', [ + 'messageChain' => $messageChain, + 'messages' => $messages, + 'totalPages' => $totalPages, + 'currentPage' => $currentPage, + ]); + } +} diff --git a/app/Community/RouteServiceProvider.php b/app/Community/RouteServiceProvider.php index 5d03aa21ee..f0f3063f8a 100755 --- a/app/Community/RouteServiceProvider.php +++ b/app/Community/RouteServiceProvider.php @@ -5,6 +5,7 @@ namespace App\Community; use App\Community\Controllers\ContactController; +use App\Community\Controllers\UserMessageChainController; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Route; @@ -228,11 +229,11 @@ protected function mapWebRoutes(): void // // Route::get('history', [PlayerHistoryController::class, 'index'])->name('history.index'); - // /* - // * inbox - // */ - // Route::resource('message', MessageController::class)->except('index', 'edit', 'update'); - // Route::resource('messages', MessageController::class)->only('index')->names(['index' => 'message.index']); + /* + * inbox + */ + Route::get('message/{chain}', UserMessageChainController::class)->name('message.view-chain'); + //Route::resource('messages', MessageController::class)->only('index')->names(['index' => 'message.index']); // /* // * tickets diff --git a/resources/views/community/components/message/breadcrumbs.blade.php b/resources/views/community/components/message/breadcrumbs.blade.php new file mode 100644 index 0000000000..723a3a8385 --- /dev/null +++ b/resources/views/community/components/message/breadcrumbs.blade.php @@ -0,0 +1,10 @@ +@props([ + 'targetUsername' => '', + 'currentPage' => '', +]) + + diff --git a/resources/views/community/components/message/view-chain-page.blade.php b/resources/views/community/components/message/view-chain-page.blade.php new file mode 100644 index 0000000000..e5ec3baf01 --- /dev/null +++ b/resources/views/community/components/message/view-chain-page.blade.php @@ -0,0 +1,61 @@ +@props([ + 'messageChain' => null, + 'messages' => [], + 'currentPage' => 1, + 'totalPages' => 1, +]) + +recipient_id); +$userFrom = User::firstWhere('ID', $messageChain->sender_id); + +$isShowAbsoluteDatesPreferenceSet = BitSet(request()->user()->websitePrefs, UserPreference::Forum_ShowAbsoluteDates); +$monthAgo = Carbon::now()->subMonth(1); + +?> + + + + +
+
+ +
+
+ +
+ @foreach ($messages as $message) +
+
+
+ {!! userAvatar($message->author_id == $userTo->ID ? $userTo->User : $userFrom->User, iconSize: 16) !!} + created_at->format('F j Y, g:ia'); ?> + @if ($isShowAbsoluteDatesPreferenceSet) + {{ $humanDate }} + @elseif ($message->created_at < $monthAgo) + {{ $humanDate }} + @else + {{ $message->created_at->diffForHumans() }} + @endif +
+
+ +

{!! Shortcode::render($message->body) !!}

+
+ @endforeach +
+ +
+ +
+ +
diff --git a/resources/views/platform/components/completion-progress-page/paginator.blade.php b/resources/views/components/paginator.blade.php similarity index 97% rename from resources/views/platform/components/completion-progress-page/paginator.blade.php rename to resources/views/components/paginator.blade.php index b5c5915980..3345cc26eb 100644 --- a/resources/views/platform/components/completion-progress-page/paginator.blade.php +++ b/resources/views/components/paginator.blade.php @@ -4,6 +4,10 @@ ]) url(); $queryParams = request()->query(); diff --git a/resources/views/platform/completion-progress-page.blade.php b/resources/views/platform/completion-progress-page.blade.php index aa0b70e82f..6cc1a9f0f4 100644 --- a/resources/views/platform/completion-progress-page.blade.php +++ b/resources/views/platform/completion-progress-page.blade.php @@ -82,7 +82,7 @@ @if ($totalPages > 0)
- +
@endif From 15530850fd55f448202811209fd5d883488967e1 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 22 Nov 2023 15:51:39 -0700 Subject: [PATCH 04/47] add inbox --- app/Community/Commands/SyncMessages.php | 10 +- .../Controllers/UserMessagesController.php | 57 ++++++++ app/Community/RouteServiceProvider.php | 3 +- app/Helpers/render/user.php | 27 ++-- ...00000_create_user_message_chains_table.php | 2 + public/request/message/delete.php | 8 +- .../components/message/list-page.blade.php | 137 ++++++++++++++++++ .../message/view-chain-page.blade.php | 18 ++- 8 files changed, 241 insertions(+), 21 deletions(-) create mode 100644 app/Community/Controllers/UserMessagesController.php create mode 100644 resources/views/community/components/message/list-page.blade.php diff --git a/app/Community/Commands/SyncMessages.php b/app/Community/Commands/SyncMessages.php index 668d78d7be..c791b26d06 100644 --- a/app/Community/Commands/SyncMessages.php +++ b/app/Community/Commands/SyncMessages.php @@ -75,10 +75,14 @@ private function migrateMessage(Message $message) } $userMessageChain->num_messages++; - if ($message->Unread) { - if ($userMessageChain->recipient_id == $userTo->ID) { + if ($userMessageChain->recipient_id == $userTo->ID) { + $userMessageChain->sender_last_post_at = $message->TimeSent; + if ($message->Unread) { $userMessageChain->recipient_num_unread++; - } else { + } + } else { + $userMessageChain->recipient_last_post_at = $message->TimeSent; + if ($message->Unread) { $userMessageChain->sender_num_unread++; } } diff --git a/app/Community/Controllers/UserMessagesController.php b/app/Community/Controllers/UserMessagesController.php new file mode 100644 index 0000000000..6df9a85d54 --- /dev/null +++ b/app/Community/Controllers/UserMessagesController.php @@ -0,0 +1,57 @@ +user(); + + $respondedMessages = UserMessageChain::where('sender_id', $user->id) + ->whereNull('sender_deleted_at') + ->whereNotNull('recipient_last_post_at'); + + $receivedMessages = UserMessageChain::where('recipient_id', $user->id) + ->whereNull('recipient_deleted_at'); + + $messages = $respondedMessages->union($receivedMessages); + $totalMessages = $messages->count(); + + $pageSize = 20; + $currentPage = (int) $request->input('page.number') ?? 1; + if ($currentPage < 1) { + $currentPage = 1; + } + $totalPages = (int) (($totalMessages + 19) / 20); + + $messages = $messages + ->orderBy(DB::raw(greatestStatement(['COALESCE(recipient_last_post_at,0)', 'sender_last_post_at'])), 'DESC') + ->offset(($currentPage - 1) * $pageSize) + ->limit($pageSize) + ->get(); + + return view('community.components.message.list-page', [ + 'messages' => $messages, + 'totalPages' => $totalPages, + 'currentPage' => $currentPage, + 'unreadCount' => $user->UnreadMessageCount, + 'totalMessages' => $totalMessages, + ]); + } +} diff --git a/app/Community/RouteServiceProvider.php b/app/Community/RouteServiceProvider.php index f0f3063f8a..39a5e52dff 100755 --- a/app/Community/RouteServiceProvider.php +++ b/app/Community/RouteServiceProvider.php @@ -6,6 +6,7 @@ use App\Community\Controllers\ContactController; use App\Community\Controllers\UserMessageChainController; +use App\Community\Controllers\UserMessagesController; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Route; @@ -233,7 +234,7 @@ protected function mapWebRoutes(): void * inbox */ Route::get('message/{chain}', UserMessageChainController::class)->name('message.view-chain'); - //Route::resource('messages', MessageController::class)->only('index')->names(['index' => 'message.index']); + Route::get('messages', UserMessagesController::class)->name('message.inbox'); // /* // * tickets diff --git a/app/Helpers/render/user.php b/app/Helpers/render/user.php index d10e2f0d1e..3b20ae4b5c 100644 --- a/app/Helpers/render/user.php +++ b/app/Helpers/render/user.php @@ -10,7 +10,7 @@ * Create the user and tooltip div that is shown when you hover over a username or user avatar. */ function userAvatar( - ?string $username, + string|User|null $user, ?bool $label = null, ?bool $icon = null, int $iconSize = 32, @@ -18,19 +18,26 @@ function userAvatar( ?string $link = null, bool|string|array $tooltip = true, ): string { - if (empty($username)) { + if (!$user) { return ''; } - $user = Cache::store('array')->remember( - CacheKey::buildUserCardDataCacheKey($username), - Carbon::now()->addMonths(3), - function () use ($username): ?array { - $foundUser = User::firstWhere('User', $username); - - return $foundUser ? $foundUser->toArray() : null; + if (is_string($user)) { + $username = $user; + if (empty($username)) { + return ''; } - ); + + $user = Cache::store('array')->remember( + CacheKey::buildUserCardDataCacheKey($username), + Carbon::now()->addMonths(3), + function () use ($username): ?array { + $foundUser = User::firstWhere('User', $username); + + return $foundUser ? $foundUser->toArray() : null; + } + ); + } if (!$user) { $userSanitized = $username; diff --git a/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php b/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php index 96026e5e14..4f59d23729 100644 --- a/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php +++ b/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php @@ -17,6 +17,8 @@ public function up(): void $table->integer('num_messages')->default(0); $table->integer('sender_num_unread')->default(0); $table->integer('recipient_num_unread')->default(0); + $table->timestampTz('sender_last_post_at')->nullable(); + $table->timestampTz('recipient_last_post_at')->nullable(); $table->timestampTz('sender_deleted_at')->nullable(); $table->timestampTz('recipient_deleted_at')->nullable(); diff --git a/public/request/message/delete.php b/public/request/message/delete.php index 650d940d3d..962ba7d10c 100644 --- a/public/request/message/delete.php +++ b/public/request/message/delete.php @@ -8,11 +8,9 @@ } $input = Validator::validate(Arr::wrap(request()->post()), [ - 'message' => 'required|integer|exists:Messages,ID', + 'message' => 'required|integer|exists:user_message_chains,ID', ]); -if (DeleteMessage($user, (int) $input['message'])) { - return back()->with('success', __('legacy.success.delete')); -} +return redirect(route('message.inbox')); -return back()->withErrors(__('legacy.error.error')); +//return back()->withErrors(__('legacy.error.error')); diff --git a/resources/views/community/components/message/list-page.blade.php b/resources/views/community/components/message/list-page.blade.php new file mode 100644 index 0000000000..79affc0df6 --- /dev/null +++ b/resources/views/community/components/message/list-page.blade.php @@ -0,0 +1,137 @@ +@props([ + 'messages' => [], + 'currentPage' => 1, + 'totalPages' => 1, + 'unreadCount' => 0, + 'totalMessages' => 0, +]) + +user(); +$isShowAbsoluteDatesPreferenceSet = BitSet($user->websitePrefs, UserPreference::Forum_ShowAbsoluteDates); +$monthAgo = Carbon::now()->subMonth(1); + +?> + + + + + + +
You have {{ $unreadCount }} unread of {{ $totalMessages }} total messages.
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + + + + + + + @foreach ($messages as $message) + sender_last_post_at); + if ($message->recipient_last_post_at) { + $mostRecentReply = Carbon::parse($message->recipient_last_post_at); + if ($mostRecentReply > $mostRecentUpdate) { + $mostRecentUpdate = $mostRecentReply; + } + } + $humanDate = $mostRecentUpdate->format('F j Y, g:ia'); + + $otherUser = ($user->id == $message->recipient_id) ? + User::firstWhere('ID', $message->sender_id) : + User::firstWhere('ID', $message->recipient_id); + + + $num_unread = 0; + if ($message->recipient_id == $user->id && $message->recipient_num_unread > 0) { + $num_unread = $message->recipient_num_unread; + } elseif ($message->sender_id == $user->id && $message->sender_num_unread > 0) { + $num_unread = $message->sender_num_unread; + } + ?> + + + + + + + + + + @endforeach +
FromTitleMessagesLast Message
+ @if ($num_unread > 0) + + @endif + {!! userAvatar($otherUser, iconSize: 24) !!} + @if ($num_unread > 0) + + @endif + + id) }}'> + @if ($num_unread > 0) + + @endif + @if ($message->sender_id == $user->id) + RE: + @endif + {{ $message->title }} + @if ($num_unread > 0) + + @endif + + + @if ($num_unread == 0) + {{ $message->num_messages }} + @elseif ($num_unread == $message->num_messages) + {{ $num_unread }} (unread) + @else + {{ $message->num_messages }} ({{ $num_unread }} unread) + @endif + + @if ($isShowAbsoluteDatesPreferenceSet) + {{ $humanDate }} + @elseif ($mostRecentUpdate < $monthAgo) + {{ $humanDate }} + @else + {{ $mostRecentUpdate->diffForHumans() }} + @endif +
+ +
+ +
+ +
diff --git a/resources/views/community/components/message/view-chain-page.blade.php b/resources/views/community/components/message/view-chain-page.blade.php index e5ec3baf01..c5cfd6ed9b 100644 --- a/resources/views/community/components/message/view-chain-page.blade.php +++ b/resources/views/community/components/message/view-chain-page.blade.php @@ -20,13 +20,27 @@ ?> + + - +
+
+ +
@@ -37,7 +51,7 @@
- {!! userAvatar($message->author_id == $userTo->ID ? $userTo->User : $userFrom->User, iconSize: 16) !!} + {!! userAvatar($message->author_id == $userTo->ID ? $userTo : $userFrom, iconSize: 16) !!} created_at->format('F j Y, g:ia'); ?> @if ($isShowAbsoluteDatesPreferenceSet) {{ $humanDate }} From 1a36a0d0ed85bab28a107da13dd16fc928374b45 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 22 Nov 2023 16:42:56 -0700 Subject: [PATCH 05/47] add outbox --- .../Controllers/UserMessagesController.php | 28 ++++++++++++++++++- app/Community/RouteServiceProvider.php | 1 + .../components/message/breadcrumbs.blade.php | 2 +- .../components/message/list-page.blade.php | 26 +++++++++++++---- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/app/Community/Controllers/UserMessagesController.php b/app/Community/Controllers/UserMessagesController.php index 6df9a85d54..19faed40dd 100644 --- a/app/Community/Controllers/UserMessagesController.php +++ b/app/Community/Controllers/UserMessagesController.php @@ -14,6 +14,7 @@ use App\Platform\Models\System; use App\Site\Models\User; use Illuminate\Contracts\View\View; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -31,6 +32,11 @@ public function __invoke(Request $request): View ->whereNull('recipient_deleted_at'); $messages = $respondedMessages->union($receivedMessages); + return $this->buildList($messages, $request, 'inbox'); + } + + private function buildList(Builder $messages, Request $request, string $mode): View + { $totalMessages = $messages->count(); $pageSize = 20; @@ -50,8 +56,28 @@ public function __invoke(Request $request): View 'messages' => $messages, 'totalPages' => $totalPages, 'currentPage' => $currentPage, - 'unreadCount' => $user->UnreadMessageCount, + 'unreadCount' => $request->user()->UnreadMessageCount, 'totalMessages' => $totalMessages, + 'mode' => $mode, ]); } + + public function outbox(Request $request): View + { + $user = $request->user(); + + $respondedMessages = UserMessageChain::where('recipient_id', $user->id) + ->whereNull('recipient_deleted_at') + ->whereRaw('recipient_last_post_at > sender_last_post_at'); + + $sentMessages = UserMessageChain::where('sender_id', $user->id) + ->whereNull('sender_deleted_at') + ->where(function ($query){ + $query->whereNull('recipient_last_post_at') + ->orWhereRaw('sender_last_post_at > recipient_last_post_at'); + }); + + $messages = $respondedMessages->union($sentMessages); + return $this->buildList($messages, $request, 'outbox'); + } } diff --git a/app/Community/RouteServiceProvider.php b/app/Community/RouteServiceProvider.php index 39a5e52dff..9310ac70fc 100755 --- a/app/Community/RouteServiceProvider.php +++ b/app/Community/RouteServiceProvider.php @@ -235,6 +235,7 @@ protected function mapWebRoutes(): void */ Route::get('message/{chain}', UserMessageChainController::class)->name('message.view-chain'); Route::get('messages', UserMessagesController::class)->name('message.inbox'); + Route::get('messages/outbox', [UserMessagesController::class, 'outbox'])->name('message.outbox'); // /* // * tickets diff --git a/resources/views/community/components/message/breadcrumbs.blade.php b/resources/views/community/components/message/breadcrumbs.blade.php index 723a3a8385..e259aa5ea1 100644 --- a/resources/views/community/components/message/breadcrumbs.blade.php +++ b/resources/views/community/components/message/breadcrumbs.blade.php @@ -4,7 +4,7 @@ ]) diff --git a/resources/views/community/components/message/list-page.blade.php b/resources/views/community/components/message/list-page.blade.php index 79affc0df6..2378f1223d 100644 --- a/resources/views/community/components/message/list-page.blade.php +++ b/resources/views/community/components/message/list-page.blade.php @@ -4,6 +4,7 @@ 'totalPages' => 1, 'unreadCount' => 0, 'totalMessages' => 0, + 'mode' => 'inbox', ]) user(); $isShowAbsoluteDatesPreferenceSet = BitSet($user->websitePrefs, UserPreference::Forum_ShowAbsoluteDates); $monthAgo = Carbon::now()->subMonth(1); @@ -31,16 +35,26 @@ function deleteMessage(id) { - - -
You have {{ $unreadCount }} unread of {{ $totalMessages }} total messages.
+ + +
+ @if ($mode == 'outbox') + You have {{ $totalMessages }} sent messages. + @else + You have {{ $unreadCount }} unread of {{ $totalMessages }} total messages. + @endif +
- + @if ($mode == 'outbox') + + @else + + @endif
@@ -53,7 +67,7 @@ function deleteMessage(id) {
- + From 4752ffa5af357a065baa661b91f9dfe9067764da Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 22 Nov 2023 21:30:41 -0700 Subject: [PATCH 06/47] support replying to messages --- .../UserMessageChainController.php | 68 +++++- app/Community/Models/UserMessageChain.php | 2 + public/request/message/create.php | 40 ++++ .../message/view-chain-page.blade.php | 12 +- ....blade.php => shortcode-buttons.blade.php} | 0 .../input/shortcode-textarea.blade.php | 60 ++++++ tests/Feature/Community/MessagesTest.php | 197 ++++++++++++++++++ 7 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 public/request/message/create.php rename resources/views/components/input/{shortcode.blade.php => shortcode-buttons.blade.php} (100%) create mode 100644 resources/views/components/input/shortcode-textarea.blade.php create mode 100644 tests/Feature/Community/MessagesTest.php diff --git a/app/Community/Controllers/UserMessageChainController.php b/app/Community/Controllers/UserMessageChainController.php index 4d880a6857..c87fb31ba6 100644 --- a/app/Community/Controllers/UserMessageChainController.php +++ b/app/Community/Controllers/UserMessageChainController.php @@ -15,6 +15,7 @@ use App\Site\Models\User; use Illuminate\Contracts\View\View; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class UserMessageChainController extends Controller @@ -30,7 +31,7 @@ public function __invoke(Request $request): View $user = $request->user(); if ($messageChain->sender_id != $user->id && $messageChain->recipient_id != $user->id) { - // TODO: abort(404); + abort(404); } $pageSize = 20; @@ -40,6 +41,15 @@ public function __invoke(Request $request): View } $totalPages = (int) (($messageChain->num_messages + 19) / 20); + if ($currentPage == $totalPages) { + if ($userMessageChain->recipient_id == $user->ID) { + $userMessageChain->recipient_num_unread = 0; + } else { + $userMessageChain->sender_num_unread = 0; + } + $userMessageChain->save(); + } + $messages = UserMessage::where('chain_id', $chainId) ->offset(($currentPage - 1) * $pageSize) ->limit($pageSize) @@ -52,4 +62,60 @@ public function __invoke(Request $request): View 'currentPage' => $currentPage, ]); } + + public static function newChain(User $userFrom, User $userTo, string $title, string $body): void + { + $userMessageChain = new UserMessageChain([ + 'title' => $title, + 'sender_id' => $userFrom->ID, + 'recipient_id' => $userTo->ID, + ]); + + UserMessageChainController::addToChain($userMessageChain, $userFrom, $body); + } + + public static function addToChain(UserMessageChain $userMessageChain, User $userFrom, string $body): void + { + $now = Carbon::now(); + + $userMessageChain->num_messages++; + if ($userMessageChain->recipient_id == $userFrom->ID) { + $userMessageChain->recipient_last_post_at = $now; + $userMessageChain->sender_num_unread++; + $userMessageChain->sender_deleted_at = null; + } else { + $userMessageChain->sender_last_post_at = $now; + $userMessageChain->recipient_num_unread++; + $userMessageChain->recipient_deleted_at = null; + } + $userMessageChain->save(); + + $userMessage = new UserMessage([ + 'chain_id' => $userMessageChain->id, + 'author_id' => $userFrom->ID, + 'body' => $body, + ]); + $userMessage->save(); + + // TODO: dispatch message sent event + // - update UnreadMessageCount + // - send email + } + + public static function deleteChain(UserMessageChain $userMessageChain, User $user): void + { + $now = Carbon::now(); + + if ($userMessageChain->recipient_id == $user->ID) { + $userMessageChain->recipient_num_unread = 0; + $userMessageChain->recipient_deleted_at = $now; + } else { + $userMessageChain->sender_num_unread = 0; + $userMessageChain->sender_deleted_at = $now; + } + + $userMessageChain->save(); + + // TODO: hard delete if both deleted_at fields are not null? + } } diff --git a/app/Community/Models/UserMessageChain.php b/app/Community/Models/UserMessageChain.php index 121d05b3cc..d9ca95932f 100644 --- a/app/Community/Models/UserMessageChain.php +++ b/app/Community/Models/UserMessageChain.php @@ -27,6 +27,8 @@ class UserMessageChain extends BaseModel protected $casts = [ 'sender_deleted_at' => 'datetime', 'recipient_deleted_at' => 'datetime', + 'sender_last_post_at' => 'datetime', + 'recipient_last_post_at' => 'datetime', ]; // == accessors diff --git a/public/request/message/create.php b/public/request/message/create.php new file mode 100644 index 0000000000..49e96b928a --- /dev/null +++ b/public/request/message/create.php @@ -0,0 +1,40 @@ +withErrors(__('legacy.error.permissions')); +} + +/** @var User $user */ +$user = request()->user(); + +$input = Validator::validate(Arr::wrap(request()->post()), [ + 'chain' => 'nullable|integer', + 'body' => 'required|string', + 'title' => 'required_without:chain', + 'recipient' => 'required_without:chain|exists:UserAccounts,User', +]); + +if (array_key_exists('chain', $input) && $input['chain'] != null) { + $userMessageChain = UserMessageChain::firstWhere('id', $input['chain']); + if (!$userMessageChain) { + return back()->withErrors(__('legacy.error.error')); + } + if ($userMessageChain->recipient_id != $user->ID && $userMessageChain->sender_id != $user->ID) { + return back()->withErrors(__('legacy.error.error')); + } + UserMessageChainController::addToChain($userMessageChain, $user, $input['body']); +} else { + $recipient = User::firstWhere('User', $input['recipient']); + UserMessageChainController::newChain($user, $recipient, $input['title'], $input['body']); +} + +return redirect(route("message.view-chain", $userMessageChain->id)); diff --git a/resources/views/community/components/message/view-chain-page.blade.php b/resources/views/community/components/message/view-chain-page.blade.php index c5cfd6ed9b..314180a8fa 100644 --- a/resources/views/community/components/message/view-chain-page.blade.php +++ b/resources/views/community/components/message/view-chain-page.blade.php @@ -18,6 +18,10 @@ $isShowAbsoluteDatesPreferenceSet = BitSet(request()->user()->websitePrefs, UserPreference::Forum_ShowAbsoluteDates); $monthAgo = Carbon::now()->subMonth(1); +$shortcodePostData = [ + 'chain' => $messageChain->id, +]; + ?> + +
+ {{ csrf_field() }} + @foreach ($postData as $key => $value) + + @endforeach + + +
+ 0 / 60000 + +
+ Loading... + + +
+
+ +
+ +
diff --git a/tests/Feature/Community/MessagesTest.php b/tests/Feature/Community/MessagesTest.php new file mode 100644 index 0000000000..3a38e4f013 --- /dev/null +++ b/tests/Feature/Community/MessagesTest.php @@ -0,0 +1,197 @@ +floorSecond(); + Carbon::setTestNow($now); + + /** @var User $user1 */ + $user1 = User::factory()->create(); + /** @var User $user2 */ + $user2 = User::factory()->create(); + + // user1 sends message to user2 + UserMessageChainController::newChain($user1, $user2, 'This is a message', 'This is the message body.'); + + $chain = UserMessageChain::firstWhere('id', 1); + $this->assertJson($chain->toJson(), json_encode([ + 'id' => 1, + 'title' => 'This is a message', + 'sender_id' => $user1->ID, + 'recipient_id' => $user2->ID, + 'num_messages' => 1, + 'sender_num_unread' => 0, + 'recipient_num_unread' => 1, + 'sender_last_post_at' => $now, + 'recipient_last_post_at' => null, + 'sender_deleted_at' => null, + 'recipient_deleted_at' => null, + ])); + $message = UserMessage::firstWhere('id', 1); + $this->assertJson($message->toJson(), json_encode([ + 'id' => 1, + 'chain_id' => 1, + 'author_id' => $user1->ID, + 'body' => 'This is the message body.', + 'created_at' => $now, + ])); + + // user2 responds + $chain->recipient_num_unread = 0; + $chain->save(); + $now2 = $now->addMinutes(5); + Carbon::setTestNow($now2); + + UserMessageChainController::addToChain($chain, $user2, 'This is a response.'); + + $chain->refresh(); + $this->assertJson($chain->toJson(), json_encode([ + 'id' => 1, + 'title' => 'This is a message', + 'sender_id' => $user1->ID, + 'recipient_id' => $user2->ID, + 'num_messages' => 2, + 'sender_num_unread' => 1, + 'recipient_num_unread' => 0, + 'sender_last_post_at' => $now, + 'recipient_last_post_at' => $now2, + 'sender_deleted_at' => null, + 'recipient_deleted_at' => null, + ])); + $message = UserMessage::firstWhere('id', 2); + $this->assertJson($message->toJson(), json_encode([ + 'id' => 2, + 'chain_id' => 1, + 'author_id' => $user2->ID, + 'body' => 'This is a response.', + 'created_at' => $now2, + ])); + + // user2 responds again + $now3 = $now->addMinutes(5); + Carbon::setTestNow($now3); + + UserMessageChainController::addToChain($chain, $user2, 'This is another response.'); + + $chain->refresh(); + $this->assertJson($chain->toJson(), json_encode([ + 'id' => 1, + 'title' => 'This is a message', + 'sender_id' => $user1->ID, + 'recipient_id' => $user2->ID, + 'num_messages' => 3, + 'sender_num_unread' => 2, + 'recipient_num_unread' => 0, + 'sender_last_post_at' => $now, + 'recipient_last_post_at' => $now3, + 'sender_deleted_at' => null, + 'recipient_deleted_at' => null, + ])); + $message = UserMessage::firstWhere('id', 3); + $this->assertJson($message->toJson(), json_encode([ + 'id' => 3, + 'chain_id' => 1, + 'author_id' => $user2->ID, + 'body' => 'This is another response.', + 'created_at' => $now3, + ])); + + // user1 responds + $chain->sender_num_unread = 0; + $chain->save(); + $now4 = $now->addMinutes(5); + Carbon::setTestNow($now4); + + UserMessageChainController::addToChain($chain, $user1, 'This is a third response.'); + + $chain->refresh(); + $this->assertJson($chain->toJson(), json_encode([ + 'id' => 1, + 'title' => 'This is a message', + 'sender_id' => $user1->ID, + 'recipient_id' => $user2->ID, + 'num_messages' => 4, + 'sender_num_unread' => 0, + 'recipient_num_unread' => 1, + 'sender_last_post_at' => $now4, + 'recipient_last_post_at' => $now3, + 'sender_deleted_at' => null, + 'recipient_deleted_at' => null, + ])); + $message = UserMessage::firstWhere('id', 4); + $this->assertJson($message->toJson(), json_encode([ + 'id' => 4, + 'chain_id' => 1, + 'author_id' => $user1->ID, + 'body' => 'This is a third response.', + 'created_at' => $now4, + ])); + + // user1 deletes + $now5 = $now->addMinutes(5); + Carbon::setTestNow($now5); + + UserMessageChainController::deleteChain($chain, $user1); + + $chain->refresh(); + $this->assertJson($chain->toJson(), json_encode([ + 'id' => 1, + 'title' => 'This is a message', + 'sender_id' => $user1->ID, + 'recipient_id' => $user2->ID, + 'num_messages' => 4, + 'sender_num_unread' => 0, + 'recipient_num_unread' => 1, + 'sender_last_post_at' => $now4, + 'recipient_last_post_at' => $now3, + 'sender_deleted_at' => $now5, + 'recipient_deleted_at' => null, + ])); + + // user2 deletes + $now6 = $now->addMinutes(5); + Carbon::setTestNow($now6); + + UserMessageChainController::deleteChain($chain, $user2); + + $chain->refresh(); + $this->assertJson($chain->toJson(), json_encode([ + 'id' => 1, + 'title' => 'This is a message', + 'sender_id' => $user1->ID, + 'recipient_id' => $user2->ID, + 'num_messages' => 4, + 'sender_num_unread' => 0, + 'recipient_num_unread' => 1, + 'sender_last_post_at' => $now4, + 'recipient_last_post_at' => $now3, + 'sender_deleted_at' => $now5, + 'recipient_deleted_at' => $now6, + ])); + } +} From da2dce67af1140cc3d9124ecb1ca085af23e77f8 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 23 Nov 2023 07:51:43 -0700 Subject: [PATCH 07/47] support creating new messages --- .../UserMessageChainController.php | 22 ++++++-- .../Controllers/UserMessagesController.php | 2 +- app/Community/RouteServiceProvider.php | 3 +- public/request/message/create.php | 8 +-- public/userInfo.php | 2 +- .../components/message/icon.blade.php | 3 +- .../components/message/list-page.blade.php | 18 +++++-- .../message/new-chain-page.blade.php | 50 +++++++++++++++++++ .../message/view-chain-page.blade.php | 22 ++++---- .../input/shortcode-textarea.blade.php | 49 ++++++++---------- .../input/user-select-image.blade.php | 15 ++++++ .../components/input/user-select.blade.php | 20 ++++++++ .../views/components/menu/account.blade.php | 3 +- 13 files changed, 160 insertions(+), 57 deletions(-) create mode 100644 resources/views/community/components/message/new-chain-page.blade.php create mode 100644 resources/views/components/input/user-select-image.blade.php create mode 100644 resources/views/components/input/user-select.blade.php diff --git a/app/Community/Controllers/UserMessageChainController.php b/app/Community/Controllers/UserMessageChainController.php index c87fb31ba6..e79bea398f 100644 --- a/app/Community/Controllers/UserMessageChainController.php +++ b/app/Community/Controllers/UserMessageChainController.php @@ -42,12 +42,14 @@ public function __invoke(Request $request): View $totalPages = (int) (($messageChain->num_messages + 19) / 20); if ($currentPage == $totalPages) { - if ($userMessageChain->recipient_id == $user->ID) { - $userMessageChain->recipient_num_unread = 0; + if ($messageChain->recipient_id == $user->ID) { + $messageChain->recipient_num_unread = 0; } else { - $userMessageChain->sender_num_unread = 0; + $messageChain->sender_num_unread = 0; } - $userMessageChain->save(); + $messageChain->save(); + + // TODO: dispatch event to update num unread messages } $messages = UserMessage::where('chain_id', $chainId) @@ -63,7 +65,16 @@ public function __invoke(Request $request): View ]); } - public static function newChain(User $userFrom, User $userTo, string $title, string $body): void + public function pageCreate(Request $request): View + { + $toUser = $request->input('to') ?? ''; + + return view('community.components.message.new-chain-page', [ + 'toUser' => $toUser + ]); + } + + public static function newChain(User $userFrom, User $userTo, string $title, string $body): UserMessageChain { $userMessageChain = new UserMessageChain([ 'title' => $title, @@ -72,6 +83,7 @@ public static function newChain(User $userFrom, User $userTo, string $title, str ]); UserMessageChainController::addToChain($userMessageChain, $userFrom, $body); + return $userMessageChain; } public static function addToChain(UserMessageChain $userMessageChain, User $userFrom, string $body): void diff --git a/app/Community/Controllers/UserMessagesController.php b/app/Community/Controllers/UserMessagesController.php index 19faed40dd..4075c0cfb5 100644 --- a/app/Community/Controllers/UserMessagesController.php +++ b/app/Community/Controllers/UserMessagesController.php @@ -62,7 +62,7 @@ private function buildList(Builder $messages, Request $request, string $mode): V ]); } - public function outbox(Request $request): View + public function pageOutbox(Request $request): View { $user = $request->user(); diff --git a/app/Community/RouteServiceProvider.php b/app/Community/RouteServiceProvider.php index 9310ac70fc..bcd3f27c26 100755 --- a/app/Community/RouteServiceProvider.php +++ b/app/Community/RouteServiceProvider.php @@ -233,9 +233,10 @@ protected function mapWebRoutes(): void /* * inbox */ + Route::get('message/new', [UserMessageChainController::class, 'pageCreate'])->name('message.new'); Route::get('message/{chain}', UserMessageChainController::class)->name('message.view-chain'); Route::get('messages', UserMessagesController::class)->name('message.inbox'); - Route::get('messages/outbox', [UserMessagesController::class, 'outbox'])->name('message.outbox'); + Route::get('messages/outbox', [UserMessagesController::class, 'pageOutbox'])->name('message.outbox'); // /* // * tickets diff --git a/public/request/message/create.php b/public/request/message/create.php index 49e96b928a..457361a1ae 100644 --- a/public/request/message/create.php +++ b/public/request/message/create.php @@ -32,9 +32,9 @@ return back()->withErrors(__('legacy.error.error')); } UserMessageChainController::addToChain($userMessageChain, $user, $input['body']); -} else { - $recipient = User::firstWhere('User', $input['recipient']); - UserMessageChainController::newChain($user, $recipient, $input['title'], $input['body']); + return redirect(route("message.view-chain", $userMessageChain->id)); } -return redirect(route("message.view-chain", $userMessageChain->id)); +$recipient = User::firstWhere('User', $input['recipient']); +$userMessageChain = UserMessageChainController::newChain($user, $recipient, $input['title'], $input['body']); +return redirect(route("message.outbox")); diff --git a/public/userInfo.php b/public/userInfo.php index 2023b1dcf2..97dbeeb04f 100644 --- a/public/userInfo.php +++ b/public/userInfo.php @@ -248,7 +248,7 @@ function resize() { echo ""; echo ""; } - echo "Message"; + echo "Message"; echo "
"; if ($areTheyFollowingMe) { diff --git a/resources/views/community/components/message/icon.blade.php b/resources/views/community/components/message/icon.blade.php index df81809308..84ff210c0f 100644 --- a/resources/views/community/components/message/icon.blade.php +++ b/resources/views/community/components/message/icon.blade.php @@ -1,6 +1,5 @@
From{{ $toFromLabel }} Title Messages Last Message
+ + + + + + + + + + + + + + +
+
+
+ +
+ +
+ +
+
+
+ +
+ + + diff --git a/resources/views/community/components/message/view-chain-page.blade.php b/resources/views/community/components/message/view-chain-page.blade.php index 314180a8fa..f23368ed38 100644 --- a/resources/views/community/components/message/view-chain-page.blade.php +++ b/resources/views/community/components/message/view-chain-page.blade.php @@ -18,10 +18,6 @@ $isShowAbsoluteDatesPreferenceSet = BitSet(request()->user()->websitePrefs, UserPreference::Forum_ShowAbsoluteDates); $monthAgo = Carbon::now()->subMonth(1); -$shortcodePostData = [ - 'chain' => $messageChain->id, -]; - ?> -
- {{ csrf_field() }} - @foreach ($postData as $key => $value) - - @endforeach - - -
- 0 / 60000 - -
- Loading... - - -
+ + +
+ 0 / 60000 + +
+ Loading... + +
+
-
- +
diff --git a/resources/views/components/input/user-select-image.blade.php b/resources/views/components/input/user-select-image.blade.php new file mode 100644 index 0000000000..05dda24ac4 --- /dev/null +++ b/resources/views/components/input/user-select-image.blade.php @@ -0,0 +1,15 @@ +@props([ + 'for' => '', + 'user' => '', + 'size' => 64, +]) + + + + diff --git a/resources/views/components/input/user-select.blade.php b/resources/views/components/input/user-select.blade.php new file mode 100644 index 0000000000..5788573863 --- /dev/null +++ b/resources/views/components/input/user-select.blade.php @@ -0,0 +1,20 @@ +@props([ + 'name' => '', + 'user' => '', +]) + + + + diff --git a/resources/views/components/menu/account.blade.php b/resources/views/components/menu/account.blade.php index c911055f3b..b43852eaa9 100644 --- a/resources/views/components/menu/account.blade.php +++ b/resources/views/components/menu/account.blade.php @@ -85,8 +85,7 @@ History {{--{{ __res('follower') }}--}} Following - {{--{{ __('Inbox') }}--}} - Messages + {{ __('Inbox') }} Reorder Site Awards {{--{{ __res('setting') }}--}} From 5c5aebcc5a0d550844e9734b174221ffa8469ab3 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 23 Nov 2023 09:00:51 -0700 Subject: [PATCH 08/47] hook up delete --- public/request/message/delete.php | 19 ++++++++++++++++--- .../message/view-chain-page.blade.php | 3 +++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/public/request/message/delete.php b/public/request/message/delete.php index 962ba7d10c..cf3a262da9 100644 --- a/public/request/message/delete.php +++ b/public/request/message/delete.php @@ -1,5 +1,7 @@ post()), [ - 'message' => 'required|integer|exists:user_message_chains,ID', + 'chain' => 'required|integer|exists:user_message_chains,id', ]); -return redirect(route('message.inbox')); +/** @var User $user */ +$user = request()->user(); -//return back()->withErrors(__('legacy.error.error')); +$userMessageChain = UserMessageChain::firstWhere('id', $input['chain']); +if (!$userMessageChain) { + return back()->withErrors(__('legacy.error.error')); +} +if ($userMessageChain->recipient_id != $user->ID && $userMessageChain->sender_id != $user->ID) { + return back()->withErrors(__('legacy.error.error')); +} + +UserMessageChainController::deleteChain($userMessageChain, $user); + +response()->json(['message' => __('legacy.success.ok')]); diff --git a/resources/views/community/components/message/view-chain-page.blade.php b/resources/views/community/components/message/view-chain-page.blade.php index f23368ed38..ef735860ae 100644 --- a/resources/views/community/components/message/view-chain-page.blade.php +++ b/resources/views/community/components/message/view-chain-page.blade.php @@ -27,6 +27,9 @@ function deleteMessage() { chain: {{ $messageChain->id }}, user: "{{ request()->user()->User }}" }) + .done(function () { + window.location.href = "{{ route('message.inbox') }}"; + }) } } From 45fccb772ac6a8b7949e631e09482570cb6e10c1 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 23 Nov 2023 09:41:07 -0700 Subject: [PATCH 09/47] automatically delete messages from blocked users --- .../UserMessageChainController.php | 24 +- app/Community/Models/UserRelation.php | 15 ++ tests/Feature/Community/MessagesTest.php | 245 ++++++++++++++---- 3 files changed, 228 insertions(+), 56 deletions(-) diff --git a/app/Community/Controllers/UserMessageChainController.php b/app/Community/Controllers/UserMessageChainController.php index e79bea398f..7d80734d77 100644 --- a/app/Community/Controllers/UserMessageChainController.php +++ b/app/Community/Controllers/UserMessageChainController.php @@ -5,9 +5,11 @@ namespace App\Community\Controllers; use App\Community\Enums\TicketState; +use App\Community\Enums\UserRelationship; use App\Community\Models\Ticket; use App\Community\Models\UserMessage; use App\Community\Models\UserMessageChain; +use App\Community\Models\UserRelation; use App\Http\Controller; use App\Platform\Models\Game; use App\Platform\Models\PlayerGame; @@ -93,12 +95,26 @@ public static function addToChain(UserMessageChain $userMessageChain, User $user $userMessageChain->num_messages++; if ($userMessageChain->recipient_id == $userFrom->ID) { $userMessageChain->recipient_last_post_at = $now; - $userMessageChain->sender_num_unread++; - $userMessageChain->sender_deleted_at = null; + + $relationship = UserRelation::getRelationship($userMessageChain->sender->User, $userFrom->User); + if ($relationship == UserRelationship::Blocked) { + $userMessageChain->sender_num_unread = 0; + $userMessageChain->sender_deleted_at = $now; + } else { + $userMessageChain->sender_num_unread++; + $userMessageChain->sender_deleted_at = null; + } } else { $userMessageChain->sender_last_post_at = $now; - $userMessageChain->recipient_num_unread++; - $userMessageChain->recipient_deleted_at = null; + + $relationship = UserRelation::getRelationship($userMessageChain->recipient->User, $userFrom->User); + if ($relationship == UserRelationship::Blocked) { + $userMessageChain->recipient_num_unread = 0; + $userMessageChain->recipient_deleted_at = $now; + } else { + $userMessageChain->recipient_num_unread++; + $userMessageChain->recipient_deleted_at = null; + } } $userMessageChain->save(); diff --git a/app/Community/Models/UserRelation.php b/app/Community/Models/UserRelation.php index fd721914fd..9a3b31c693 100644 --- a/app/Community/Models/UserRelation.php +++ b/app/Community/Models/UserRelation.php @@ -4,6 +4,7 @@ namespace App\Community\Models; +use App\Community\Enums\UserRelationship; use App\Support\Database\Eloquent\BaseModel; class UserRelation extends BaseModel @@ -15,11 +16,25 @@ class UserRelation extends BaseModel public const CREATED_AT = 'Created'; public const UPDATED_AT = 'Updated'; + protected $fillable = [ + 'User', + 'Friend', + 'Friendship', + ]; + // == accessors // == mutators // == relations + public static function getRelationship(string $user, string $relatedUser): int + { + $relation = UserRelation::where('User', $user) + ->where('Friend', $relatedUser) + ->first(); + return $relation ? $relation->Friendship : UserRelationship::NotFollowing; + } + // == scopes } diff --git a/tests/Feature/Community/MessagesTest.php b/tests/Feature/Community/MessagesTest.php index 3a38e4f013..23cf53bb57 100644 --- a/tests/Feature/Community/MessagesTest.php +++ b/tests/Feature/Community/MessagesTest.php @@ -6,8 +6,10 @@ use App\Community\Controllers\UserMessageChainController; use App\Community\Enums\AwardType; +use App\Community\Enums\UserRelationship; use App\Community\Models\UserMessage; use App\Community\Models\UserMessageChain; +use App\Community\Models\UserRelation; use App\Platform\Enums\UnlockMode; use App\Platform\Models\Achievement; use App\Platform\Models\Game; @@ -36,10 +38,8 @@ public function testCreateMessageChain(): void $user2 = User::factory()->create(); // user1 sends message to user2 - UserMessageChainController::newChain($user1, $user2, 'This is a message', 'This is the message body.'); - - $chain = UserMessageChain::firstWhere('id', 1); - $this->assertJson($chain->toJson(), json_encode([ + $chain = UserMessageChainController::newChain($user1, $user2, 'This is a message', 'This is the message body.'); + $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', 'sender_id' => $user1->ID, @@ -47,30 +47,28 @@ public function testCreateMessageChain(): void 'num_messages' => 1, 'sender_num_unread' => 0, 'recipient_num_unread' => 1, - 'sender_last_post_at' => $now, + 'sender_last_post_at' => $now->toDateTimeString(), 'recipient_last_post_at' => null, 'sender_deleted_at' => null, 'recipient_deleted_at' => null, - ])); - $message = UserMessage::firstWhere('id', 1); - $this->assertJson($message->toJson(), json_encode([ + ]); + $this->assertDatabaseHas('user_messages', [ 'id' => 1, 'chain_id' => 1, 'author_id' => $user1->ID, 'body' => 'This is the message body.', - 'created_at' => $now, - ])); + 'created_at' => $now->toDateTimeString(), + ]); // user2 responds $chain->recipient_num_unread = 0; $chain->save(); - $now2 = $now->addMinutes(5); + $now2 = $now->clone()->addMinutes(5); Carbon::setTestNow($now2); UserMessageChainController::addToChain($chain, $user2, 'This is a response.'); - $chain->refresh(); - $this->assertJson($chain->toJson(), json_encode([ + $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', 'sender_id' => $user1->ID, @@ -78,28 +76,26 @@ public function testCreateMessageChain(): void 'num_messages' => 2, 'sender_num_unread' => 1, 'recipient_num_unread' => 0, - 'sender_last_post_at' => $now, - 'recipient_last_post_at' => $now2, + 'sender_last_post_at' => $now->toDateTimeString(), + 'recipient_last_post_at' => $now2->toDateTimeString(), 'sender_deleted_at' => null, 'recipient_deleted_at' => null, - ])); - $message = UserMessage::firstWhere('id', 2); - $this->assertJson($message->toJson(), json_encode([ + ]); + $this->assertDatabaseHas('user_messages', [ 'id' => 2, 'chain_id' => 1, 'author_id' => $user2->ID, 'body' => 'This is a response.', - 'created_at' => $now2, - ])); + 'created_at' => $now2->toDateTimeString(), + ]); // user2 responds again - $now3 = $now->addMinutes(5); + $now3 = $now2->clone()->addMinutes(5); Carbon::setTestNow($now3); UserMessageChainController::addToChain($chain, $user2, 'This is another response.'); - $chain->refresh(); - $this->assertJson($chain->toJson(), json_encode([ + $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', 'sender_id' => $user1->ID, @@ -107,30 +103,29 @@ public function testCreateMessageChain(): void 'num_messages' => 3, 'sender_num_unread' => 2, 'recipient_num_unread' => 0, - 'sender_last_post_at' => $now, - 'recipient_last_post_at' => $now3, + 'sender_last_post_at' => $now->toDateTimeString(), + 'recipient_last_post_at' => $now3->toDateTimeString(), 'sender_deleted_at' => null, 'recipient_deleted_at' => null, - ])); - $message = UserMessage::firstWhere('id', 3); - $this->assertJson($message->toJson(), json_encode([ + ]); + $this->assertDatabaseHas('user_messages', [ 'id' => 3, 'chain_id' => 1, 'author_id' => $user2->ID, 'body' => 'This is another response.', 'created_at' => $now3, - ])); + ]); // user1 responds $chain->sender_num_unread = 0; $chain->save(); - $now4 = $now->addMinutes(5); + $now4 = $now3->clone()->addMinutes(5); Carbon::setTestNow($now4); UserMessageChainController::addToChain($chain, $user1, 'This is a third response.'); $chain->refresh(); - $this->assertJson($chain->toJson(), json_encode([ + $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', 'sender_id' => $user1->ID, @@ -138,28 +133,26 @@ public function testCreateMessageChain(): void 'num_messages' => 4, 'sender_num_unread' => 0, 'recipient_num_unread' => 1, - 'sender_last_post_at' => $now4, - 'recipient_last_post_at' => $now3, + 'sender_last_post_at' => $now4->toDateTimeString(), + 'recipient_last_post_at' => $now3->toDateTimeString(), 'sender_deleted_at' => null, 'recipient_deleted_at' => null, - ])); - $message = UserMessage::firstWhere('id', 4); - $this->assertJson($message->toJson(), json_encode([ + ]); + $this->assertDatabaseHas('user_messages', [ 'id' => 4, 'chain_id' => 1, 'author_id' => $user1->ID, 'body' => 'This is a third response.', - 'created_at' => $now4, - ])); + 'created_at' => $now4->toDateTimeString(), + ]); // user1 deletes - $now5 = $now->addMinutes(5); + $now5 = $now4->clone()->addMinutes(5); Carbon::setTestNow($now5); UserMessageChainController::deleteChain($chain, $user1); - $chain->refresh(); - $this->assertJson($chain->toJson(), json_encode([ + $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', 'sender_id' => $user1->ID, @@ -167,31 +160,179 @@ public function testCreateMessageChain(): void 'num_messages' => 4, 'sender_num_unread' => 0, 'recipient_num_unread' => 1, - 'sender_last_post_at' => $now4, - 'recipient_last_post_at' => $now3, - 'sender_deleted_at' => $now5, + 'sender_last_post_at' => $now4->toDateTimeString(), + 'recipient_last_post_at' => $now3->toDateTimeString(), + 'sender_deleted_at' => $now5->toDateTimeString(), 'recipient_deleted_at' => null, - ])); + ]); // user2 deletes - $now6 = $now->addMinutes(5); + $now6 = $now5->clone()->addMinutes(5); Carbon::setTestNow($now6); UserMessageChainController::deleteChain($chain, $user2); $chain->refresh(); - $this->assertJson($chain->toJson(), json_encode([ + $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', 'sender_id' => $user1->ID, 'recipient_id' => $user2->ID, 'num_messages' => 4, 'sender_num_unread' => 0, + 'recipient_num_unread' => 0, + 'sender_last_post_at' => $now4->toDateTimeString(), + 'recipient_last_post_at' => $now3->toDateTimeString(), + 'sender_deleted_at' => $now5->toDateTimeString(), + 'recipient_deleted_at' => $now6->toDateTimeString(), + ]); + } + + public function testBlockedUser(): void + { + $now = Carbon::now()->floorSecond(); + Carbon::setTestNow($now); + + /** @var User $user1 */ + $user1 = User::factory()->create(); + /** @var User $user2 */ + $user2 = User::factory()->create(); + + // user1 has user2 blocked + $relation = new UserRelation([ + 'User' => $user1->User, + 'Friend' => $user2->User, + 'Friendship' => UserRelationship::Blocked, + ]); + $relation->save(); + + // message from user2 is automatically marked as deleted by user1 + $chain = UserMessageChainController::newChain($user2, $user1, 'This is a message', 'This is the message body.'); + $this->assertDatabaseHas('user_message_chains', [ + 'id' => 1, + 'title' => 'This is a message', + 'sender_id' => $user2->ID, + 'recipient_id' => $user1->ID, + 'num_messages' => 1, + 'sender_num_unread' => 0, + 'recipient_num_unread' => 0, + 'sender_last_post_at' => $now->toDateTimeString(), + 'recipient_last_post_at' => null, + 'sender_deleted_at' => null, + 'recipient_deleted_at' => $now->toDateTimeString(), + ]); + $this->assertDatabaseHas('user_messages', [ + 'id' => 1, + 'chain_id' => 1, + 'author_id' => $user2->ID, + 'body' => 'This is the message body.', + 'created_at' => $now->toDateTimeString(), + ]); + + // additional message from user2 is also marked as deleted by user1 + $now2 = $now->clone()->addMinutes(5); + Carbon::setTestNow($now2); + + UserMessageChainController::addToChain($chain, $user2, 'This is a response.'); + + $this->assertDatabaseHas('user_message_chains',[ + 'id' => 1, + 'title' => 'This is a message', + 'sender_id' => $user2->ID, + 'recipient_id' => $user1->ID, + 'num_messages' => 2, + 'sender_num_unread' => 0, + 'recipient_num_unread' => 0, + 'sender_last_post_at' => $now2->toDateTimeString(), + 'recipient_last_post_at' => null, + 'sender_deleted_at' => null, + 'recipient_deleted_at' => $now2->toDateTimeString(), + ]); + $this->assertDatabaseHas('user_messages', [ + 'id' => 2, + 'chain_id' => 1, + 'author_id' => $user2->ID, + 'body' => 'This is a response.', + 'created_at' => $now2->toDateTimeString(), + ]); + + // message from user1 is delivered to user2 + Carbon::setTestNow($now); + + $chain = UserMessageChainController::newChain($user1, $user2, 'This is a message', 'This is the message body.'); + $this->assertDatabaseHas('user_message_chains',[ + 'id' => 2, + 'title' => 'This is a message', + 'sender_id' => $user1->ID, + 'recipient_id' => $user2->ID, + 'num_messages' => 1, + 'sender_num_unread' => 0, 'recipient_num_unread' => 1, - 'sender_last_post_at' => $now4, - 'recipient_last_post_at' => $now3, - 'sender_deleted_at' => $now5, - 'recipient_deleted_at' => $now6, - ])); + 'sender_last_post_at' => $now->toDateTimeString(), + 'recipient_last_post_at' => null, + 'sender_deleted_at' => null, + 'recipient_deleted_at' => null, + ]); + $message = UserMessage::firstWhere('id', 1); + $this->assertDatabaseHas('user_messages', [ + 'id' => 3, + 'chain_id' => 2, + 'author_id' => $user1->ID, + 'body' => 'This is the message body.', + 'created_at' => $now->toDateTimeString(), + ]); + + // additional message from user1 is also delivered + Carbon::setTestNow($now2); + + UserMessageChainController::addToChain($chain, $user1, 'This is a response.'); + + $this->assertDatabaseHas('user_message_chains',[ + 'id' => 2, + 'title' => 'This is a message', + 'sender_id' => $user1->ID, + 'recipient_id' => $user2->ID, + 'num_messages' => 2, + 'sender_num_unread' => 0, + 'recipient_num_unread' => 2, + 'sender_last_post_at' => $now2->toDateTimeString(), + 'recipient_last_post_at' => null, + 'sender_deleted_at' => null, + 'recipient_deleted_at' => null, + ]); + $this->assertDatabaseHas('user_messages', [ + 'id' => 4, + 'chain_id' => 2, + 'author_id' => $user1->ID, + 'body' => 'This is a response.', + 'created_at' => $now2->toDateTimeString(), + ]); + + // response from user2 is automatically deleted + $now3 = $now2->clone()->addMinutes(5); + Carbon::setTestNow($now3); + + UserMessageChainController::addToChain($chain, $user2, 'This is another response.'); + + $this->assertDatabaseHas('user_message_chains',[ + 'id' => 2, + 'title' => 'This is a message', + 'sender_id' => $user1->ID, + 'recipient_id' => $user2->ID, + 'num_messages' => 3, + 'sender_num_unread' => 0, + 'recipient_num_unread' => 2, + 'sender_last_post_at' => $now2->toDateTimeString(), + 'recipient_last_post_at' => $now3->toDateTimeString(), + 'sender_deleted_at' => $now3->toDateTimeString(), + 'recipient_deleted_at' => null, + ]); + $this->assertDatabaseHas('user_messages', [ + 'id' => 5, + 'chain_id' => 2, + 'author_id' => $user2->ID, + 'body' => 'This is another response.', + 'created_at' => $now3->toDateTimeString(), + ]); } } From 7243944825687f3194662d18a941668f5fc5e436 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 23 Nov 2023 12:01:01 -0700 Subject: [PATCH 10/47] support for deleted users --- app/Site/Actions/ClearAccountDataAction.php | 3 +++ .../components/message/list-page.blade.php | 5 ++-- .../message/view-chain-page.blade.php | 24 +++++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/Site/Actions/ClearAccountDataAction.php b/app/Site/Actions/ClearAccountDataAction.php index f3d3d37175..fb45892f74 100644 --- a/app/Site/Actions/ClearAccountDataAction.php +++ b/app/Site/Actions/ClearAccountDataAction.php @@ -39,6 +39,9 @@ public function execute(User $user): void // TODO $user->subscriptions()->delete(); DB::statement('DELETE FROM Subscription WHERE UserID = :userId', ['userId' => $user->ID]); + DB::statement('UPDATE user_message_chains SET sender_deleted_at=NOW() WHERE sender_id = :userId', ['userId' => $user->ID]); + DB::statement('UPDATE user_message_chains SET recipient_deleted_at=NOW() WHERE recipient_id = :userId', ['userId' => $user->ID]); + DB::statement("UPDATE UserAccounts u SET u.Password = null, u.SaltedPass = '', diff --git a/resources/views/community/components/message/list-page.blade.php b/resources/views/community/components/message/list-page.blade.php index b614392e7b..674099772f 100644 --- a/resources/views/community/components/message/list-page.blade.php +++ b/resources/views/community/components/message/list-page.blade.php @@ -88,9 +88,8 @@ function deleteMessage(id) { $humanDate = $mostRecentUpdate->format('F j Y, g:ia'); $otherUser = ($user->id == $message->recipient_id) ? - User::firstWhere('ID', $message->sender_id) : - User::firstWhere('ID', $message->recipient_id); - + User::withTrashed()->firstWhere('ID', $message->sender_id) : + User::withTrashed()->firstWhere('ID', $message->recipient_id); $num_unread = 0; if ($message->recipient_id == $user->id && $message->recipient_num_unread > 0) { diff --git a/resources/views/community/components/message/view-chain-page.blade.php b/resources/views/community/components/message/view-chain-page.blade.php index ef735860ae..4c0a16a770 100644 --- a/resources/views/community/components/message/view-chain-page.blade.php +++ b/resources/views/community/components/message/view-chain-page.blade.php @@ -12,8 +12,8 @@ use App\Support\Shortcode\Shortcode; use Illuminate\Support\Carbon; -$userTo = User::firstWhere('ID', $messageChain->recipient_id); -$userFrom = User::firstWhere('ID', $messageChain->sender_id); +$userTo = User::withTrashed()->firstWhere('ID', $messageChain->recipient_id); +$userFrom = User::withTrashed()->firstWhere('ID', $messageChain->sender_id); $isShowAbsoluteDatesPreferenceSet = BitSet(request()->user()->websitePrefs, UserPreference::Forum_ShowAbsoluteDates); $monthAgo = Carbon::now()->subMonth(1); @@ -75,15 +75,19 @@ function deleteMessage() { @endforeach
-
- {{ csrf_field() }} - + @if ($userFrom->trashed() || $userTo->trashed()) +
Cannot reply to deleted user.
+ @else + + {{ csrf_field() }} + - - + + + @endif
From a96e9c16607323ed81cb54eda6e04993297fada5 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 23 Nov 2023 20:05:47 -0700 Subject: [PATCH 11/47] update UnreadMessageCount --- .../UserMessageChainController.php | 55 +++++++++++++++---- app/Site/Models/User.php | 1 + tests/Feature/Community/MessagesTest.php | 45 +++++++++++++-- 3 files changed, 86 insertions(+), 15 deletions(-) diff --git a/app/Community/Controllers/UserMessageChainController.php b/app/Community/Controllers/UserMessageChainController.php index 7d80734d77..0500f6729e 100644 --- a/app/Community/Controllers/UserMessageChainController.php +++ b/app/Community/Controllers/UserMessageChainController.php @@ -44,14 +44,8 @@ public function __invoke(Request $request): View $totalPages = (int) (($messageChain->num_messages + 19) / 20); if ($currentPage == $totalPages) { - if ($messageChain->recipient_id == $user->ID) { - $messageChain->recipient_num_unread = 0; - } else { - $messageChain->sender_num_unread = 0; - } - $messageChain->save(); - - // TODO: dispatch event to update num unread messages + // if viewing last page, mark all messages in the chain as read + UserMessageChainController::markRead($messageChain, $user); } $messages = UserMessage::where('chain_id', $chainId) @@ -104,6 +98,8 @@ public static function addToChain(UserMessageChain $userMessageChain, User $user $userMessageChain->sender_num_unread++; $userMessageChain->sender_deleted_at = null; } + + $userTo = User::firstWhere('id', $userMessageChain->sender_id); } else { $userMessageChain->sender_last_post_at = $now; @@ -115,6 +111,8 @@ public static function addToChain(UserMessageChain $userMessageChain, User $user $userMessageChain->recipient_num_unread++; $userMessageChain->recipient_deleted_at = null; } + + $userTo = User::firstWhere('id', $userMessageChain->recipient_id); } $userMessageChain->save(); @@ -125,9 +123,42 @@ public static function addToChain(UserMessageChain $userMessageChain, User $user ]); $userMessage->save(); - // TODO: dispatch message sent event - // - update UnreadMessageCount - // - send email + UserMessageChainController::updateUnreadMessageCount($userTo); + + // TODO: send email + } + + private static function updateUnreadMessageCount(User $user): void + { + $unreadReplies = UserMessageChain::where('sender_id', $user->id) + ->whereNull('sender_deleted_at') + ->sum('sender_num_unread'); + + $unreadMessages = UserMessageChain::where('recipient_id', $user->id) + ->whereNull('recipient_deleted_at') + ->sum('recipient_num_unread'); + + $user->UnreadMessageCount = $unreadMessages + $unreadReplies; + $user->save(); + } + + public static function markRead(UserMessageChain $userMessageChain, User $user): void + { + if ($userMessageChain->recipient_id == $user->ID) { + if ($userMessageChain->recipient_num_unread) { + $userMessageChain->recipient_num_unread = 0; + $userMessageChain->save(); + + UserMessageChainController::updateUnreadMessageCount($user); + } + } else { + if ($userMessageChain->sender_num_unread) { + $userMessageChain->sender_num_unread = 0; + $userMessageChain->save(); + + UserMessageChainController::updateUnreadMessageCount($user); + } + } } public static function deleteChain(UserMessageChain $userMessageChain, User $user): void @@ -145,5 +176,7 @@ public static function deleteChain(UserMessageChain $userMessageChain, User $use $userMessageChain->save(); // TODO: hard delete if both deleted_at fields are not null? + + UserMessageChainController::updateUnreadMessageCount($user); } } diff --git a/app/Site/Models/User.php b/app/Site/Models/User.php index c21dcbbd2e..09dda6e848 100644 --- a/app/Site/Models/User.php +++ b/app/Site/Models/User.php @@ -158,6 +158,7 @@ class User extends Authenticatable implements CommunityMember, Developer, HasCom "LastLogin", "avatarUrl", 'Created', + 'UnreadMessageCount', ]; protected $appends = [ diff --git a/tests/Feature/Community/MessagesTest.php b/tests/Feature/Community/MessagesTest.php index 23cf53bb57..05768a7448 100644 --- a/tests/Feature/Community/MessagesTest.php +++ b/tests/Feature/Community/MessagesTest.php @@ -60,9 +60,14 @@ public function testCreateMessageChain(): void 'created_at' => $now->toDateTimeString(), ]); + $user2->refresh(); + $this->assertEquals(1, $user2->UnreadMessageCount); + // user2 responds - $chain->recipient_num_unread = 0; - $chain->save(); + UserMessageChainController::markRead($chain, $user2); + $user2->refresh(); + $this->assertEquals(0, $user2->UnreadMessageCount); + $now2 = $now->clone()->addMinutes(5); Carbon::setTestNow($now2); @@ -89,6 +94,9 @@ public function testCreateMessageChain(): void 'created_at' => $now2->toDateTimeString(), ]); + $user1->refresh(); + $this->assertEquals(1, $user1->UnreadMessageCount); + // user2 responds again $now3 = $now2->clone()->addMinutes(5); Carbon::setTestNow($now3); @@ -116,9 +124,14 @@ public function testCreateMessageChain(): void 'created_at' => $now3, ]); + $user1->refresh(); + $this->assertEquals(2, $user1->UnreadMessageCount); + // user1 responds - $chain->sender_num_unread = 0; - $chain->save(); + UserMessageChainController::markRead($chain, $user1); + $user1->refresh(); + $this->assertEquals(0, $user1->UnreadMessageCount); + $now4 = $now3->clone()->addMinutes(5); Carbon::setTestNow($now4); @@ -146,6 +159,9 @@ public function testCreateMessageChain(): void 'created_at' => $now4->toDateTimeString(), ]); + $user2->refresh(); + $this->assertEquals(1, $user2->UnreadMessageCount); + // user1 deletes $now5 = $now4->clone()->addMinutes(5); Carbon::setTestNow($now5); @@ -166,6 +182,9 @@ public function testCreateMessageChain(): void 'recipient_deleted_at' => null, ]); + $user2->refresh(); + $this->assertEquals(1, $user2->UnreadMessageCount); + // user2 deletes $now6 = $now5->clone()->addMinutes(5); Carbon::setTestNow($now6); @@ -186,6 +205,9 @@ public function testCreateMessageChain(): void 'sender_deleted_at' => $now5->toDateTimeString(), 'recipient_deleted_at' => $now6->toDateTimeString(), ]); + + $user2->refresh(); + $this->assertEquals(0, $user2->UnreadMessageCount); } public function testBlockedUser(): void @@ -229,6 +251,9 @@ public function testBlockedUser(): void 'created_at' => $now->toDateTimeString(), ]); + $user1->refresh(); + $this->assertEquals(0, $user1->UnreadMessageCount); + // additional message from user2 is also marked as deleted by user1 $now2 = $now->clone()->addMinutes(5); Carbon::setTestNow($now2); @@ -256,6 +281,9 @@ public function testBlockedUser(): void 'created_at' => $now2->toDateTimeString(), ]); + $user1->refresh(); + $this->assertEquals(0, $user1->UnreadMessageCount); + // message from user1 is delivered to user2 Carbon::setTestNow($now); @@ -282,6 +310,9 @@ public function testBlockedUser(): void 'created_at' => $now->toDateTimeString(), ]); + $user2->refresh(); + $this->assertEquals(1, $user2->UnreadMessageCount); + // additional message from user1 is also delivered Carbon::setTestNow($now2); @@ -308,6 +339,9 @@ public function testBlockedUser(): void 'created_at' => $now2->toDateTimeString(), ]); + $user2->refresh(); + $this->assertEquals(2, $user2->UnreadMessageCount); + // response from user2 is automatically deleted $now3 = $now2->clone()->addMinutes(5); Carbon::setTestNow($now3); @@ -334,5 +368,8 @@ public function testBlockedUser(): void 'body' => 'This is another response.', 'created_at' => $now3->toDateTimeString(), ]); + + $user1->refresh(); + $this->assertEquals(0, $user1->UnreadMessageCount); } } From c2419ec619522ec75135a90c833ca8906ee20414 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 23 Nov 2023 21:20:26 -0700 Subject: [PATCH 12/47] send private message emails --- .../UserMessageChainController.php | 7 +- app/Helpers/util/mail.php | 14 ++- tests/Feature/Community/MessagesTest.php | 34 +++++-- tests/Feature/Site/Concerns/TestsMail.php | 90 +++++++++++++++++++ 4 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 tests/Feature/Site/Concerns/TestsMail.php diff --git a/app/Community/Controllers/UserMessageChainController.php b/app/Community/Controllers/UserMessageChainController.php index 0500f6729e..2179b71f16 100644 --- a/app/Community/Controllers/UserMessageChainController.php +++ b/app/Community/Controllers/UserMessageChainController.php @@ -14,6 +14,7 @@ use App\Platform\Models\Game; use App\Platform\Models\PlayerGame; use App\Platform\Models\System; +use App\Site\Enums\UserPreference; use App\Site\Models\User; use Illuminate\Contracts\View\View; use Illuminate\Http\Request; @@ -125,7 +126,11 @@ public static function addToChain(UserMessageChain $userMessageChain, User $user UserMessageChainController::updateUnreadMessageCount($userTo); - // TODO: send email + // send email? + if (BitSet($userTo->websitePrefs, UserPreference::EmailOn_PrivateMessage) && + $relationship != UserRelationship::Blocked) { + sendPrivateMessageEmail($userTo->User, $userTo->EmailAddress, $userMessageChain->title, $body, $userFrom->User); + } } private static function updateUnreadMessageCount(User $user): void diff --git a/app/Helpers/util/mail.php b/app/Helpers/util/mail.php index 9ba4ad08a9..7df03f40db 100644 --- a/app/Helpers/util/mail.php +++ b/app/Helpers/util/mail.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Mail\Mailer as MailerContract; use Illuminate\Mail\Mailer; use Illuminate\Mail\Transport\SesTransport; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; use Symfony\Component\Mime\Email; @@ -33,7 +34,16 @@ function mail_utf8(string $to, string $subject = '(No subject)', string $message function mail_log(string $to, string $subject = '(No subject)', string $message = ''): bool { - Log::debug('Mail', ['to' => $to, 'subject' => $subject, 'message' => $message]); + $mailParams = ['to' => $to, 'subject' => $subject, 'message' => $message]; + Log::debug('Mail', $mailParams); + + if (app()->environment('testing')) { + $arr = Cache::store('array')->get('test:emails'); + if ($arr !== null) { + $arr[] = $mailParams; + Cache::store('array')->put('test:emails', $arr); + } + } return true; } @@ -353,7 +363,7 @@ function SendPrivateMessageEmail( // Also used for Generic text: $emailTitle = "New Private Message from $fromUser"; - $link = "here"; + $link = "here"; $msg = "Hello $user!
" . "You have received a new private message from $fromUser.

" . diff --git a/tests/Feature/Community/MessagesTest.php b/tests/Feature/Community/MessagesTest.php index 05768a7448..6d5f2dc50f 100644 --- a/tests/Feature/Community/MessagesTest.php +++ b/tests/Feature/Community/MessagesTest.php @@ -16,16 +16,18 @@ use App\Platform\Models\PlayerBadge; use App\Platform\Models\System; use App\Site\Enums\Permissions; +use App\Site\Enums\UserPreference; use App\Site\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Carbon; -use Tests\Feature\Platform\Concerns\TestsPlayerAchievements; -use Tests\Feature\Platform\Concerns\TestsPlayerBadges; +use Illuminate\Support\Facades\Log; +use Tests\Feature\Site\Concerns\TestsMail; use Tests\TestCase; class MessagesTest extends TestCase { use RefreshDatabase; + use TestsMail; public function testCreateMessageChain(): void { @@ -35,9 +37,10 @@ public function testCreateMessageChain(): void /** @var User $user1 */ $user1 = User::factory()->create(); /** @var User $user2 */ - $user2 = User::factory()->create(); + $user2 = User::factory()->create(['websitePrefs' => (1 << UserPreference::EmailOn_PrivateMessage)]); // user1 sends message to user2 + $this->captureEmails(); $chain = UserMessageChainController::newChain($user1, $user2, 'This is a message', 'This is the message body.'); $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, @@ -63,6 +66,8 @@ public function testCreateMessageChain(): void $user2->refresh(); $this->assertEquals(1, $user2->UnreadMessageCount); + $this->assertEmailSent($user2, "New Private Message from {$user1->User}"); + // user2 responds UserMessageChainController::markRead($chain, $user2); $user2->refresh(); @@ -71,8 +76,8 @@ public function testCreateMessageChain(): void $now2 = $now->clone()->addMinutes(5); Carbon::setTestNow($now2); + $this->captureEmails(); UserMessageChainController::addToChain($chain, $user2, 'This is a response.'); - $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', @@ -97,12 +102,14 @@ public function testCreateMessageChain(): void $user1->refresh(); $this->assertEquals(1, $user1->UnreadMessageCount); + $this->assertEmailNotSent($user1); + // user2 responds again $now3 = $now2->clone()->addMinutes(5); Carbon::setTestNow($now3); + $this->captureEmails(); UserMessageChainController::addToChain($chain, $user2, 'This is another response.'); - $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', @@ -127,6 +134,8 @@ public function testCreateMessageChain(): void $user1->refresh(); $this->assertEquals(2, $user1->UnreadMessageCount); + $this->assertEmailNotSent($user1); + // user1 responds UserMessageChainController::markRead($chain, $user1); $user1->refresh(); @@ -135,9 +144,8 @@ public function testCreateMessageChain(): void $now4 = $now3->clone()->addMinutes(5); Carbon::setTestNow($now4); + $this->captureEmails(); UserMessageChainController::addToChain($chain, $user1, 'This is a third response.'); - - $chain->refresh(); $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', @@ -162,12 +170,14 @@ public function testCreateMessageChain(): void $user2->refresh(); $this->assertEquals(1, $user2->UnreadMessageCount); + $this->assertEmailSent($user2, "New Private Message from {$user1->User}"); + // user1 deletes $now5 = $now4->clone()->addMinutes(5); Carbon::setTestNow($now5); + $this->captureEmails(); UserMessageChainController::deleteChain($chain, $user1); - $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, 'title' => 'This is a message', @@ -185,12 +195,15 @@ public function testCreateMessageChain(): void $user2->refresh(); $this->assertEquals(1, $user2->UnreadMessageCount); + $this->assertEmailNotSent($user1); + $this->assertEmailNotSent($user2); + // user2 deletes $now6 = $now5->clone()->addMinutes(5); Carbon::setTestNow($now6); + $this->captureEmails(); UserMessageChainController::deleteChain($chain, $user2); - $chain->refresh(); $this->assertDatabaseHas('user_message_chains', [ 'id' => 1, @@ -208,6 +221,9 @@ public function testCreateMessageChain(): void $user2->refresh(); $this->assertEquals(0, $user2->UnreadMessageCount); + + $this->assertEmailNotSent($user1); + $this->assertEmailNotSent($user2); } public function testBlockedUser(): void diff --git a/tests/Feature/Site/Concerns/TestsMail.php b/tests/Feature/Site/Concerns/TestsMail.php new file mode 100644 index 0000000000..602f80e738 --- /dev/null +++ b/tests/Feature/Site/Concerns/TestsMail.php @@ -0,0 +1,90 @@ +put('test:emails', []); + } + + protected function assertEmailSent(User $user, string $subject, string $body = null) + { + $emails = Cache::store('array')->get('test:emails') ?? []; + $matchUser = null; + $matchSubject = null; + $matchBoth = null; + foreach ($emails as $email) { + if ($email['to'] === $user->EmailAddress && $email['subject'] === $subject) { + if ($body === null || $body === $email['message']) { + return; + } + + $matchBoth = $email; + } elseif ($email['to'] === $user->EmailAddress) { + $matchUser = $email; + } elseif ($email['subject'] === $subject) { + $matchSubject = $email; + } + } + + $expected = [ + 'to' => $user->EmailAddress, + 'subject' => $subject, + 'message' => $body, + ]; + + if ($matchBoth) { + $this->assertEquals($expected, $matchBoth); + } elseif ($matchUser) { + $this->assertEquals($expected, $matchUser); + } elseif ($matchSubject) { + $this->assertEquals($expected, $matchSubject); + } else { + $this->fail("Expected email sent to {$user->EmailAddress}. No emails captured."); + } + } + + protected function assertEmailNotSent(User $user, string $subject = null, string $body = null) + { + $emails = Cache::store('array')->get('test:emails') ?? []; + $matchUser = null; + $matchUserAndSubject = null; + $matchAll = null; + foreach ($emails as $email) { + if ($email['to'] === $user->EmailAddress) { + if ($subject !== null && $subject === $email['subject']) { + if ($body !== null && $body === $email['message']) { + $matchAll = $email; + } else { + $matchUserAndSubject = $email; + } + } else { + $matchUser = $email; + } + } + } + + $expected = [ + 'to' => $user->EmailAddress, + 'subject' => $subject, + 'message' => $body, + ]; + + if ($matchAll) { + $this->fail("Found email sent to {$user->EmailAddress} with specified subject and body"); + } elseif ($matchUserAndSubject) { + $this->fail("Found email sent to {$user->EmailAddress} with specified subject"); + } elseif ($matchUser) { + $this->fail("Found email sent to {$user->EmailAddress}"); + } + } +} From 84d1c8af08c699f714990a256c651b8ea0525151 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 23 Nov 2023 21:31:01 -0700 Subject: [PATCH 13/47] eliminate inbox.php --- app/Site/Components/NotificationIcon.php | 2 +- public/inbox.php | 187 ----------------------- public/request/message/create.php | 6 +- public/request/message/read.php | 19 --- public/request/message/send.php | 26 ---- 5 files changed, 4 insertions(+), 236 deletions(-) delete mode 100644 public/inbox.php delete mode 100644 public/request/message/read.php delete mode 100644 public/request/message/send.php diff --git a/app/Site/Components/NotificationIcon.php b/app/Site/Components/NotificationIcon.php index b38e5b33af..42f8be04a3 100644 --- a/app/Site/Components/NotificationIcon.php +++ b/app/Site/Components/NotificationIcon.php @@ -24,7 +24,7 @@ public function render(): View if ($user->unread_messages_count) { $notifications->push([ - 'link' => url('inbox.php'), + 'link' => route('message.inbox'), 'title' => $user->unread_messages_count . ' ' . __res('message', (int) $user->unread_messages_count), ]); } diff --git a/public/inbox.php b/public/inbox.php deleted file mode 100644 index 259f439ac8..0000000000 --- a/public/inbox.php +++ /dev/null @@ -1,187 +0,0 @@ -input('u'); -$displayCount = requestInputSanitized('c', 10, 'integer'); -$offset = requestInputSanitized('o', 0, 'integer'); - -if (!authenticateFromCookie($user, $permissions, $userDetails)) { - abort(401); -} - -if ($outbox) { - $unreadMessageCount = 0; - $totalMessageCount = GetSentMessageCount($user); - $allMessages = GetSentMessages($user, $offset, $displayCount); - RenderContentStart('Outbox'); -} else { - $unreadMessageCount = GetMessageCount($user, $totalMessageCount); - $allMessages = GetAllMessages($user, $offset, $displayCount, $unreadOnly); - RenderContentStart('Inbox'); -} -?> - -
-
- Outbox"; - - echo "
You have $totalMessageCount sent messages.
"; - - echo "
"; - echo "Inbox"; - echo "
"; - echo "Create New Message"; - echo "
"; - echo "
"; - } else { - echo "

Inbox

"; - - echo "
"; - echo "
You have $unreadMessageCount unread of $totalMessageCount total messages.
"; - echo "
"; - - echo "
"; - echo "Outbox"; - echo "
"; - if ($unreadOnly) { - echo "View All Messages"; - } else { - echo "View Unread Only"; - } - echo "Create New Message"; - echo "
"; - echo "
"; - } - - echo ""; - - echo ""; - echo ""; - if ($outbox) { - echo ""; - } else { - echo ""; - } - echo ""; - echo ""; - - $totalMsgs = count($allMessages); - - for ($i = 0; $i < $totalMsgs; $i++) { - $msgID = $allMessages[$i]['ID']; - $msgTime = $allMessages[$i]['TimeSent']; - $msgSentAtNice = date("d/m/y, H:i ", strtotime($msgTime)); - // $msgTo = $allMessages[$i]['UserTo']; - $msgTitle = $allMessages[$i]['Title']; - $msgPayload = $allMessages[$i]['Payload']; - $msgType = $allMessages[$i]['Type']; - - if ($outbox) { - $msgUser = $allMessages[$i]['UserTo']; - $msgUnread = false; - } else { - $msgUser = $allMessages[$i]['UserFrom']; - $msgUnread = ($allMessages[$i]['Unread'] == 1); - } - - sanitize_outputs( - $msgUser, - $msgTitle, - $msgPayload, - ); - - $msgPayload = Shortcode::render($msgPayload); - - $styleAlt = $i % 2 == 1 ? "alt" : ""; - - echo ""; - - echo ""; - - echo ""; - echo ""; - - // echo ""; - - echo ""; - - echo ""; - - echo ""; - echo ""; - echo ""; - } - - echo "
DateToFromTitle
"; - echo "$msgSentAtNice"; - echo ""; - echo userAvatar($msgUser, label: false); - echo ""; - echo userAvatar($msgUser, icon: false); - echo "" . $msgTo . ""; - echo $msgTitle; - echo "
"; - echo "
$msgPayload
"; - - if (!$outbox) { - echo "
"; - - echo "
"; - echo csrf_field(); - echo ""; - echo ""; - echo "
"; - - echo ""; - - echo "Reply"; - - echo "
"; - } - - echo "
"; - - // Get message count and build paginator URL from current GET parameters - $messageCount = $unreadOnly ? $unreadMessageCount : $totalMessageCount; - unset($_GET['o']); - $urlPrefix = '/inbox.php?' . http_build_query($_GET) . '&o='; - - echo "
"; - RenderPaginator($messageCount, $displayCount, $offset, $urlPrefix); - echo "
"; - - ?> -
-
- diff --git a/public/request/message/create.php b/public/request/message/create.php index 457361a1ae..a5fc11b390 100644 --- a/public/request/message/create.php +++ b/public/request/message/create.php @@ -18,8 +18,8 @@ $input = Validator::validate(Arr::wrap(request()->post()), [ 'chain' => 'nullable|integer', - 'body' => 'required|string', - 'title' => 'required_without:chain', + 'body' => 'required|string|max:60000', + 'title' => 'required_without:chain|string|max:255', 'recipient' => 'required_without:chain|exists:UserAccounts,User', ]); @@ -37,4 +37,4 @@ $recipient = User::firstWhere('User', $input['recipient']); $userMessageChain = UserMessageChainController::newChain($user, $recipient, $input['title'], $input['body']); -return redirect(route("message.outbox")); +return redirect(route("message.outbox"))->with('success', __('legacy.success.message_send')); diff --git a/public/request/message/read.php b/public/request/message/read.php deleted file mode 100644 index 332ebe4eb3..0000000000 --- a/public/request/message/read.php +++ /dev/null @@ -1,19 +0,0 @@ -post()), [ - 'message' => 'required|integer|exists:Messages,ID', - 'status' => 'required|integer|in:0,1', -]); - -if (markMessageAsRead($user, $input['message'], $input['status'])) { - return true; -} - -abort(400); diff --git a/public/request/message/send.php b/public/request/message/send.php deleted file mode 100644 index c35b776a59..0000000000 --- a/public/request/message/send.php +++ /dev/null @@ -1,26 +0,0 @@ -withErrors(__('legacy.error.permissions')); -} - -$input = Validator::validate(Arr::wrap(request()->post()), [ - 'recipient' => 'required|string|exists:UserAccounts,User', - 'subject' => 'required|string|max:255', - 'message' => 'required|string|max:60000', -]); - -$recipient = $input['recipient']; - -if (isUserBlocking($recipient, $username)) { - return redirect(url('inbox.php?s=1'))->with('success', __('legacy.success.message_send')); -} - -if (CreateNewMessage($username, $recipient, $input['subject'], $input['message'])) { - return redirect(url('inbox.php?s=1'))->with('success', __('legacy.success.message_send')); -} - -return back()->withErrors(__('legacy.error.error')); From 021331788003e6fd047545cccdb09dc3decd0225 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 23 Nov 2023 21:44:32 -0700 Subject: [PATCH 14/47] eliminate createmessage.php --- .../UserMessageChainController.php | 6 +- app/Helpers/render/ticket.php | 2 +- config/missing-page-redirector.php | 1 - public/createmessage.php | 106 ------------------ public/friends.php | 2 +- public/managehashes.php | 2 +- public/ticketmanager.php | 4 +- .../message/new-chain-page.blade.php | 5 +- resources/views/community/contact.blade.php | 16 +-- 9 files changed, 22 insertions(+), 122 deletions(-) delete mode 100644 public/createmessage.php diff --git a/app/Community/Controllers/UserMessageChainController.php b/app/Community/Controllers/UserMessageChainController.php index 2179b71f16..1132bd700a 100644 --- a/app/Community/Controllers/UserMessageChainController.php +++ b/app/Community/Controllers/UserMessageChainController.php @@ -65,9 +65,13 @@ public function __invoke(Request $request): View public function pageCreate(Request $request): View { $toUser = $request->input('to') ?? ''; + $subject = $request->input('subject') ?? ''; + $message = $request->input('message') ?? ''; return view('community.components.message.new-chain-page', [ - 'toUser' => $toUser + 'toUser' => $toUser, + 'subject' => $subject, + 'message' => $message, ]); } diff --git a/app/Helpers/render/ticket.php b/app/Helpers/render/ticket.php index aa24f82c75..20622d2bc7 100644 --- a/app/Helpers/render/ticket.php +++ b/app/Helpers/render/ticket.php @@ -31,7 +31,7 @@ function ticketAvatar( resource: 'ticket', id: $ticket->id, label: "Ticket #{$ticket->id}", - link: 'ticketmanager.php?i=' . $ticket->id, + link: '/ticketmanager.php?i=' . $ticket->id, tooltip: is_array($tooltip) ? renderAchievementCard($tooltip) : $tooltip, class: "ticket-avatar $ticketStateClass", iconUrl: media_asset("/Badge/" . $ticket->badgeName . ".png"), diff --git a/config/missing-page-redirector.php b/config/missing-page-redirector.php index 752a437274..b5097abe57 100644 --- a/config/missing-page-redirector.php +++ b/config/missing-page-redirector.php @@ -90,7 +90,6 @@ */ '/controlpanel.php' => '/settings', '/friends.php' => '/friends', - '/createmessage.php' => '/message/create', '/inbox.php' => '/messages', '/gamecompare.php' => '/user/{f}/game/{ID}/compare', '/manageuserpic.php' => '/settings', diff --git a/public/createmessage.php b/public/createmessage.php deleted file mode 100644 index 63f37a1751..0000000000 --- a/public/createmessage.php +++ /dev/null @@ -1,106 +0,0 @@ - - -
- "; - echo "Inbox"; - echo " » Send Message"; - echo "
"; - - echo "

New Message

"; - - if ($messageContextData !== null) { - echo "In reply to "; - echo userAvatar($messageContextData['UserFrom']); - echo " who wrote:

"; - echo "
$messageContextPayload
"; - } - - echo "
"; - echo csrf_field(); - - echo ""; - echo ""; - - $destUser = mb_strlen($messageTo) > 2 ? $messageTo : '_User'; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - - $loadingIconSrc = asset('assets/images/icon/loading.gif'); - - echo << - - - - HTML; - - echo ""; - echo "
User:
Subject:
Message:"; - RenderShortcodeButtons(); - echo "
-
- Loading... - - -
-
"; - echo "
"; - - echo "
"; - ?> - - diff --git a/public/friends.php b/public/friends.php index 1c1645318d..707d8ba31d 100644 --- a/public/friends.php +++ b/public/friends.php @@ -124,7 +124,7 @@ function RenderUserList(string $header, array $users, int $friendshipType, array echo ""; echo "
"; - echo "Message"; + echo "Message"; echo "
"; echo csrf_field(); diff --git a/public/managehashes.php b/public/managehashes.php index 1199bb3756..6483ab0004 100644 --- a/public/managehashes.php +++ b/public/managehashes.php @@ -113,7 +113,7 @@ function UnlinkHash(user, gameID, hash, elem) { } echo "
"; - echo "
Warning: PLEASE be careful with this tool. If in doubt, leave a message for admins and they'll help sort it.
"; + echo "
Warning: PLEASE be careful with this tool. If in doubt, leave a message for admins and they'll help sort it.
"; echo "
Currently this game has $numLinks unique hashes registered for it:
"; diff --git a/public/ticketmanager.php b/public/ticketmanager.php index ebf055c3c8..ed0da96aee 100644 --- a/public/ticketmanager.php +++ b/public/ticketmanager.php @@ -726,10 +726,10 @@ echo "Reporter:"; echo ""; echo "
"; - $msgPayload = "Hi [user=$reportedBy], I'm contacting you about ticket retroachievements.org/ticketmanager.php?i=$ticketID "; + $msgPayload = "Hi [user=$reportedBy], I'm contacting you about [ticket=$ticketID]"; $msgPayload = rawurlencode($msgPayload); $msgTitle = rawurlencode("Bug Report ($gameTitle)"); - echo "Contact the reporter - $reportedBy"; + echo "Contact the reporter - $reportedBy"; echo "
"; echo ""; echo ""; diff --git a/resources/views/community/components/message/new-chain-page.blade.php b/resources/views/community/components/message/new-chain-page.blade.php index e3321756ce..79ccd61162 100644 --- a/resources/views/community/components/message/new-chain-page.blade.php +++ b/resources/views/community/components/message/new-chain-page.blade.php @@ -1,5 +1,7 @@ @props([ 'toUser' => '', + 'subject' => '', + 'message' => '', ]) - + @@ -41,6 +43,7 @@ diff --git a/resources/views/community/contact.blade.php b/resources/views/community/contact.blade.php index a4b137d984..0b5039d699 100644 --- a/resources/views/community/contact.blade.php +++ b/resources/views/community/contact.blade.php @@ -5,7 +5,7 @@

Admins and Moderators

- Send a message to RAdmin for: + Send a message to RAdmin for:

    @@ -20,7 +20,7 @@

    Developer Compliance

    - Send a message to DevCompliance for: + Send a message to DevCompliance for:

      @@ -34,7 +34,7 @@

      Quality Assurance

      - Send a message to QATeam for: + Send a message to QATeam for:

        @@ -51,7 +51,7 @@

        DevQuest

        - Send a message to DevQuest for + Send a message to DevQuest for submissions, questions, ideas, or reporting issues related to DevQuest.

        @@ -63,7 +63,7 @@

        Cheating Reports

        - Send a message to RACheats + Send a message to RACheats if you believe someone is in violation of our Global Leaderboard and Achievement Hunting Rules.

        @@ -81,7 +81,7 @@

        RANews

        - Send a message to RANews for: + Send a message to RANews for:

          @@ -96,13 +96,13 @@

          RAEvents

          - Send a message to RAEvents for + Send a message to RAEvents for submissions, questions, ideas, or reporting issues related to community events.

          - Send a message to TheUnwanted for + Send a message to TheUnwanted for submissions, questions, ideas, or reporting issues specifically related to The Unwanted.

          From f663c6db2268183abb6a7a1f973b8c5ef1a19fd1 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 23 Nov 2023 22:04:17 -0700 Subject: [PATCH 15/47] eliminate database/message.php --- app/Helpers/database/message.php | 217 ----------------------------- app/Helpers/database/ticket.php | 12 +- app/Helpers/database/user-auth.php | 2 +- app/Helpers/helpers.php | 1 - 4 files changed, 9 insertions(+), 223 deletions(-) delete mode 100644 app/Helpers/database/message.php diff --git a/app/Helpers/database/message.php b/app/Helpers/database/message.php deleted file mode 100644 index 18bf97a02c..0000000000 --- a/app/Helpers/database/message.php +++ /dev/null @@ -1,217 +0,0 @@ - $username]); - - foreach ($data as $datum) { - if ($datum['Unread'] == 1) { - $unreadMessageCount = (int) $datum['NumFound']; - } - - $totalMessageCount += (int) $datum['NumFound']; - } - - return $unreadMessageCount; -} - -function GetSentMessageCount(string $user): int -{ - sanitize_sql_inputs($user); - - if (!isset($user)) { - return 0; - } - - $messageCount = 0; - - $query = " - SELECT COUNT(*) AS NumFound - FROM Messages AS msg - WHERE msg.UserFrom = '$user' - "; - - $dbResult = s_mysql_query($query); - - if ($dbResult !== false) { - while ($data = mysqli_fetch_assoc($dbResult)) { - $messageCount = (int) $data['NumFound']; - } - } - - return $messageCount; -} - -function GetMessage(string $user, int $id): ?array -{ - sanitize_sql_inputs($user); - - $query = "SELECT * FROM Messages AS msg - WHERE msg.ID='$id' AND msg.UserTo='$user'"; - - $dbResult = s_mysql_query($query); - - if (!$dbResult) { - return null; - } - - $numFound = mysqli_num_rows($dbResult); - if ($numFound > 0) { - return mysqli_fetch_assoc($dbResult); - } - - return null; -} - -function GetAllMessages(string $user, int $offset, int $count, bool $unreadOnly): array -{ - sanitize_sql_inputs($user); - - $retval = []; - - $subQuery = ''; - if ($unreadOnly) { - $subQuery = " AND msg.Unread=1"; - } - - $query = "SELECT * FROM Messages AS msg - WHERE msg.UserTo='$user' $subQuery - ORDER BY msg.TimeSent DESC - LIMIT $offset, $count"; - - $dbResult = s_mysql_query($query); - - if ($dbResult !== false) { - while ($data = mysqli_fetch_assoc($dbResult)) { - $retval[] = $data; - } - } else { - log_sql_fail(); - } - - return $retval; -} - -function GetSentMessages(string $user, int $offset, int $count): array -{ - sanitize_sql_inputs($user); - - $retval = []; - - $query = "SELECT * FROM Messages AS msg - WHERE msg.UserFrom='$user' - ORDER BY msg.TimeSent DESC - LIMIT $offset, $count"; - - $dbResult = s_mysql_query($query); - - if ($dbResult !== false) { - while ($data = mysqli_fetch_assoc($dbResult)) { - $retval[] = $data; - } - } - - return $retval; -} - -function UpdateCachedUnreadTotals(string $user): void -{ - sanitize_sql_inputs($user); - - $query = " - UPDATE UserAccounts AS ua - SET UnreadMessageCount = ( - SELECT COUNT(*) FROM - ( - SELECT * - FROM Messages AS msg - WHERE msg.UserTo = '$user' AND msg.Unread = 1 - ) InnerTable - ), Updated=NOW() WHERE ua.User = '$user'"; - - s_mysql_query($query); -} - -function markMessageAsRead(string $user, int $messageID, bool $setAsUnread = false): bool -{ - sanitize_sql_inputs($user); - - $newReadStatus = (int) $setAsUnread; - - $query = "UPDATE Messages AS msg - SET msg.Unread=$newReadStatus - WHERE msg.ID = $messageID AND msg.UserTo = '$user'"; - - $dbResult = s_mysql_query($query); - - if ($dbResult !== false) { - UpdateCachedUnreadTotals($user); - } - - return $dbResult !== false; -} - -function DeleteMessage(string $user, int $messageID): bool -{ - sanitize_sql_inputs($user); - - $messageToDelete = GetMessage($user, $messageID); - - if (!$messageToDelete) { - return false; - } - - if (strtolower($messageToDelete['UserTo']) !== strtolower($user)) { - return false; - } - - $query = "DELETE FROM Messages WHERE Messages.ID = $messageID"; - $dbResult = s_mysql_query($query); - if ($dbResult !== false) { - UpdateCachedUnreadTotals($user); - } - - return $dbResult !== false; -} diff --git a/app/Helpers/database/ticket.php b/app/Helpers/database/ticket.php index f70bab23a8..6614b6bff8 100644 --- a/app/Helpers/database/ticket.php +++ b/app/Helpers/database/ticket.php @@ -1,5 +1,6 @@ save(); + } // notify subscribers other than the achievement's author // TODO dry it. why is this not (1 << 1) like in submitNewTicketsJSON? diff --git a/app/Helpers/database/user-auth.php b/app/Helpers/database/user-auth.php index df8b0cd196..37d4dc07d2 100644 --- a/app/Helpers/database/user-auth.php +++ b/app/Helpers/database/user-auth.php @@ -85,7 +85,7 @@ function authenticateForConnect(?string $username, ?string $pass = null, ?string 'Token' => $user->appToken, 'Score' => $user->RAPoints, 'SoftcoreScore' => $user->RASoftcorePoints, - 'Messages' => GetMessageCount($user, $totalMessageCount), + 'Messages' => $user->UnreadMessageCount, 'Permissions' => $permissions, 'AccountType' => Permissions::toString($permissions), ]; diff --git a/app/Helpers/helpers.php b/app/Helpers/helpers.php index 4bbdbf2110..e30ba14f3d 100644 --- a/app/Helpers/helpers.php +++ b/app/Helpers/helpers.php @@ -11,7 +11,6 @@ require_once __DIR__ . '/database/game.php'; require_once __DIR__ . '/database/hash.php'; require_once __DIR__ . '/database/leaderboard.php'; -require_once __DIR__ . '/database/message.php'; require_once __DIR__ . '/database/player-achievement.php'; require_once __DIR__ . '/database/player-game.php'; require_once __DIR__ . '/database/player-history.php'; From 3d5f7d6cd0eedac9e6957fdfd671aa138e3f4f55 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Fri, 24 Nov 2023 07:55:04 -0700 Subject: [PATCH 16/47] hard-delete message when both users delete it --- .../UserMessageChainController.php | 11 ++++++---- tests/Feature/Community/MessagesTest.php | 21 ++++++------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/app/Community/Controllers/UserMessageChainController.php b/app/Community/Controllers/UserMessageChainController.php index 1132bd700a..9cb10ea7e7 100644 --- a/app/Community/Controllers/UserMessageChainController.php +++ b/app/Community/Controllers/UserMessageChainController.php @@ -181,10 +181,13 @@ public static function deleteChain(UserMessageChain $userMessageChain, User $use $userMessageChain->sender_num_unread = 0; $userMessageChain->sender_deleted_at = $now; } - - $userMessageChain->save(); - - // TODO: hard delete if both deleted_at fields are not null? + + if ($userMessageChain->recipient_deleted_at != null && $userMessageChain->sender_deleted_at != null) { + // this will also cascade delete the user_messages + $userMessageChain->delete(); + } else { + $userMessageChain->save(); + } UserMessageChainController::updateUnreadMessageCount($user); } diff --git a/tests/Feature/Community/MessagesTest.php b/tests/Feature/Community/MessagesTest.php index 6d5f2dc50f..e5cb432493 100644 --- a/tests/Feature/Community/MessagesTest.php +++ b/tests/Feature/Community/MessagesTest.php @@ -198,26 +198,17 @@ public function testCreateMessageChain(): void $this->assertEmailNotSent($user1); $this->assertEmailNotSent($user2); - // user2 deletes + // user2 deletes - when both users delete the message, it's removed from the DB $now6 = $now5->clone()->addMinutes(5); Carbon::setTestNow($now6); $this->captureEmails(); UserMessageChainController::deleteChain($chain, $user2); - $chain->refresh(); - $this->assertDatabaseHas('user_message_chains', [ - 'id' => 1, - 'title' => 'This is a message', - 'sender_id' => $user1->ID, - 'recipient_id' => $user2->ID, - 'num_messages' => 4, - 'sender_num_unread' => 0, - 'recipient_num_unread' => 0, - 'sender_last_post_at' => $now4->toDateTimeString(), - 'recipient_last_post_at' => $now3->toDateTimeString(), - 'sender_deleted_at' => $now5->toDateTimeString(), - 'recipient_deleted_at' => $now6->toDateTimeString(), - ]); + $this->assertDatabaseMissing('user_message_chains', ['id' => 1]); + $this->assertDatabaseMissing('user_messages', ['id' => 1]); + $this->assertDatabaseMissing('user_messages', ['id' => 2]); + $this->assertDatabaseMissing('user_messages', ['id' => 3]); + $this->assertDatabaseMissing('user_messages', ['id' => 4]); $user2->refresh(); $this->assertEquals(0, $user2->UnreadMessageCount); From 27936f07d3af483964afb57beda54e4bd9fee8c8 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sat, 25 Nov 2023 23:28:45 -0700 Subject: [PATCH 17/47] rename tables --- app/Community/Commands/SyncMessages.php | 116 ++++-- .../Controllers/MessageThreadsController.php | 253 ++++++++++++ .../UserMessageChainController.php | 194 ---------- .../Controllers/UserMessagesController.php | 83 ---- app/Community/EventServiceProvider.php | 10 + app/Community/Events/MessageCreated.php | 28 ++ .../Listeners/NotifyMessageParticipants.php | 66 ++++ app/Community/Models/Message.php | 50 +-- app/Community/Models/MessageThread.php | 27 ++ .../Models/MessageThreadParticipant.php | 31 ++ app/Community/Models/UserMessage.php | 47 --- app/Community/Models/UserMessageChain.php | 57 --- app/Community/Models/UserRelation.php | 1 + app/Community/RouteServiceProvider.php | 12 +- app/Helpers/database/ticket.php | 7 +- app/Helpers/database/user-auth.php | 2 +- app/Helpers/util/mail.php | 2 +- app/Site/Components/NotificationIcon.php | 2 +- ...00000_create_user_message_chains_table.php | 37 -- ...1_22_000001_create_user_messages_table.php | 38 -- ...25_000000_create_message_threads_table.php | 24 ++ ...001_create_message_thread_participants.php | 28 ++ ...023_11_25_000002_update_messages_table.php | 117 ++++++ public/managehashes.php | 2 +- public/request/message/create.php | 33 +- public/request/message/delete.php | 26 +- .../components/message/breadcrumbs.blade.php | 10 - .../components/message/icon.blade.php | 2 +- .../components/message/list-page.blade.php | 65 +--- ...ge.blade.php => new-thread-page.blade.php} | 5 +- ...e.blade.php => view-thread-page.blade.php} | 71 ++-- .../components/user/breadcrumbs.blade.php | 6 + .../views/components/menu/account.blade.php | 2 +- tests/Feature/Community/MessagesTest.php | 361 ++++++++++-------- tests/Feature/Site/Concerns/TestsMail.php | 4 +- 35 files changed, 1000 insertions(+), 819 deletions(-) create mode 100644 app/Community/Controllers/MessageThreadsController.php delete mode 100644 app/Community/Controllers/UserMessageChainController.php delete mode 100644 app/Community/Controllers/UserMessagesController.php create mode 100644 app/Community/Events/MessageCreated.php create mode 100644 app/Community/Listeners/NotifyMessageParticipants.php create mode 100644 app/Community/Models/MessageThread.php create mode 100644 app/Community/Models/MessageThreadParticipant.php delete mode 100644 app/Community/Models/UserMessage.php delete mode 100644 app/Community/Models/UserMessageChain.php delete mode 100644 database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php delete mode 100644 database/migrations/platform/2023_11_22_000001_create_user_messages_table.php create mode 100644 database/migrations/platform/2023_11_25_000000_create_message_threads_table.php create mode 100644 database/migrations/platform/2023_11_25_000001_create_message_thread_participants.php create mode 100644 database/migrations/platform/2023_11_25_000002_update_messages_table.php delete mode 100644 resources/views/community/components/message/breadcrumbs.blade.php rename resources/views/community/components/message/{new-chain-page.blade.php => new-thread-page.blade.php} (89%) rename resources/views/community/components/message/{view-chain-page.blade.php => view-thread-page.blade.php} (51%) diff --git a/app/Community/Commands/SyncMessages.php b/app/Community/Commands/SyncMessages.php index c791b26d06..ef6e713348 100644 --- a/app/Community/Commands/SyncMessages.php +++ b/app/Community/Commands/SyncMessages.php @@ -5,10 +5,13 @@ namespace App\Community\Commands; use App\Community\Models\Message; -use App\Community\Models\UserMessage; -use App\Community\Models\UserMessageChain; +use App\Community\Models\MessageThread; +use App\Community\Models\MessageThreadParticipant; use App\Site\Models\User; use Illuminate\Console\Command; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class SyncMessages extends Command { @@ -22,7 +25,7 @@ public function __construct() public function handle(): void { - $count = Message::whereNull('migrated_id')->count(); + $count = Message::where('thread_id', 0)->count(); $progressBar = $this->output->createProgressBar($count); $progressBar->start(); @@ -30,7 +33,7 @@ public function handle(): void // have to do this in batches to prevent exhausting memory // due to requesting payloads (message content) for ($i = 0; $i < $count; $i += 100) { - $messages = Message::whereNull('migrated_id')->orderBy('ID')->limit(100)->get(); + $messages = Message::where('thread_id', 0)->orderBy('ID')->limit(100)->get(); /** @var Message $message */ foreach ($messages as $message) { $this->migrateMessage($message); @@ -40,63 +43,94 @@ public function handle(): void $progressBar->finish(); $this->line(PHP_EOL); + + $count = Message::where('thread_id', 0)->count(); + if ($count == 0) { + // all messages sync'd. add the foreign key so deletes will cascade + $sm = Schema::getConnection()->getDoctrineSchemaManager(); + $foreignKeysFound = $sm->listTableForeignKeys('messages'); + + $found = false; + foreach ($foreignKeysFound as $foreignKey) { + if ($foreignKey->getName() == 'messages_thread_id_foreign') { + $found = true; + break; + } + } + + if (!$found) { + Schema::table('messages', function (Blueprint $table) { + $table->foreign('thread_id')->references('ID')->on('message_threads')->onDelete('cascade'); + }); + } + } + + // automatically mark bug report notifications as deleted by the sender if + // the recipient hasn't replied to them. + DB::statement("UPDATE message_thread_participants mtp + INNER JOIN message_threads mt ON mt.id=mtp.thread_id + INNER JOIN messages m ON m.thread_id=mtp.thread_id AND m.author_id=mtp.user_id + SET mtp.deleted_at=NOW() + WHERE mt.num_messages=1 AND mt.title LIKE 'Bug Report (%'"); } private function migrateMessage(Message $message) { - $userFrom = User::where('User', $message->UserFrom)->first(); - $userTo = User::where('User', $message->UserTo)->first(); - if (!$userFrom || !$userTo) { - // sender or recipient was deleted. ignore message - $message->migrated_id = 0; - $message->save(); + // recipient can be entered by user. trim whitespace before trying to match + $message->UserTo = trim($message->UserTo); + $userTo = User::withTrashed()->where('User', $message->UserTo)->first(); + if (!$userTo) { + $message->delete(); + return; } + $thread = null; if (strtolower(substr($message->Title, 0, 4)) == 're: ') { - $parent = Message::where('Title', '=', substr($message->Title, 4)) + $threadId = Message::where('title', '=', substr($message->Title, 4)) ->where('UserFrom', '=', $message->UserTo) ->where('UserTo', '=', $message->UserFrom) - ->where('ID', '<', $message->ID) - ->first(); - } else { - $parent = null; + ->where('id', '<', $message->id) + ->value('thread_id'); + if ($threadId > 0) { + $thread = MessageThread::firstWhere('id', $threadId); + } } - if ($parent === null) { - $userMessageChain = new UserMessageChain([ + if ($thread === null) { + $thread = new MessageThread([ 'title' => $message->Title, - 'sender_id' => $userFrom->ID, - 'recipient_id' => $userTo->ID, ]); - } else { - $migratedParent = UserMessage::where('id', $parent->migrated_id)->firstOrFail(); - $userMessageChain = UserMessageChain::where('id', $migratedParent->chain_id)->firstOrFail(); - } + $thread->save(); + + $participantTo = new MessageThreadParticipant([ + 'user_id' => $userTo->ID, + 'thread_id' => $thread->id, + ]); + $participantTo->save(); - $userMessageChain->num_messages++; - if ($userMessageChain->recipient_id == $userTo->ID) { - $userMessageChain->sender_last_post_at = $message->TimeSent; - if ($message->Unread) { - $userMessageChain->recipient_num_unread++; + if ($message->author_id != $userTo->ID) { + $participantFrom = new MessageThreadParticipant([ + 'user_id' => $message->author_id, + 'thread_id' => $thread->id, + ]); + $participantFrom->save(); } } else { - $userMessageChain->recipient_last_post_at = $message->TimeSent; - if ($message->Unread) { - $userMessageChain->sender_num_unread++; - } + $threadParticipants = MessageThreadParticipant::where('thread_id', $thread->id); + $participantTo = $threadParticipants->where('user_id', $userTo->ID)->first(); + } + + if ($message->Unread) { + $participantTo->num_unread++; + $participantTo->save(); } - $userMessageChain->save(); - $userMessage = new UserMessage([ - 'chain_id' => $userMessageChain->id, - 'author_id' => $userFrom->ID, - 'body' => $message->Payload, - 'created_at' => $message->TimeSent, - ]); - $userMessage->save(); + $thread->num_messages++; + $thread->last_message_id = $message->id; + $thread->save(); - $message->migrated_id = $userMessage->id; + $message->thread_id = $thread->id; $message->save(); } } diff --git a/app/Community/Controllers/MessageThreadsController.php b/app/Community/Controllers/MessageThreadsController.php new file mode 100644 index 0000000000..180a7181b1 --- /dev/null +++ b/app/Community/Controllers/MessageThreadsController.php @@ -0,0 +1,253 @@ +user(); + + $messageThreads = MessageThread::join('message_thread_participants', 'message_thread_participants.thread_id', '=', 'message_threads.id') + ->where('message_thread_participants.user_id', $user->ID) + ->whereNull('message_thread_participants.deleted_at'); + $totalMessages = $messageThreads->count(); + + $pageSize = 20; + $currentPage = (int) $request->input('page.number') ?? 1; + if ($currentPage < 1) { + $currentPage = 1; + } + $totalPages = (int) (($totalMessages + 19) / 20); + + $messageThreads = $messageThreads + ->join('messages', 'messages.id', '=', 'message_threads.last_message_id') + ->orderBy('messages.created_at', 'DESC') + ->offset(($currentPage - 1) * $pageSize) + ->limit($pageSize) + ->select(['message_threads.*', + DB::raw('messages.author_id AS last_author_id'), + DB::raw('messages.created_at AS last_message_at'), + ]) + ->get(); + + foreach ($messageThreads as &$messageThread) { + $messageThread['other_participants'] = User::withTrashed() + ->join('message_thread_participants', 'message_thread_participants.user_id', '=', 'UserAccounts.ID') + ->where('message_thread_participants.thread_id', '=', $messageThread->id) + ->where('message_thread_participants.user_id', '!=', $user->ID) + ->pluck('UserAccounts.User') + ->toArray(); + $messageThread['num_unread'] = MessageThreadParticipant::where('user_id', $user->ID) + ->where('thread_id', $messageThread->id) + ->value('num_unread'); + } + + return view('community.components.message.list-page', [ + 'messages' => $messageThreads, + 'totalPages' => $totalPages, + 'currentPage' => $currentPage, + 'unreadCount' => $request->user()->UnreadMessageCount, + 'totalMessages' => $totalMessages, + ]); + } + + public function pageView(Request $request): View + { + $threadId = $request->route('thread_id'); + + $thread = MessageThread::firstWhere('id', $threadId); + if (!$thread) { + abort(404); + } + + $user = $request->user(); + $participant = MessageThreadParticipant::where('thread_id', $threadId) + ->where('user_id', $user->ID) + ->first(); + if (!$participant) { + abort(404); + } + + $pageSize = 20; + $currentPage = (int) $request->input('page.number') ?? 1; + if ($currentPage < 1) { + $currentPage = 1; + } + $totalPages = (int) (($thread->num_messages + 19) / 20); + + if ($currentPage == $totalPages) { + // if viewing last page, mark all messages in the chain as read + MessageThreadsController::markParticipantRead($participant, $user); + } + + $messages = Message::where('thread_id', $threadId) + ->orderBy('created_at') + ->offset(($currentPage - 1) * $pageSize) + ->limit($pageSize) + ->get(); + + $participants = MessageThreadParticipant::withTrashed() + ->where('thread_id', $thread->id) + ->join('UserAccounts', 'UserAccounts.ID', '=', 'message_thread_participants.user_id'); + + $canReply = with(clone $participants) + ->where('user_id', '!=', $user->ID) + ->whereNull('UserAccounts.Deleted') + ->exists(); + + $participants = $participants->get(['UserAccounts.ID', 'UserAccounts.User']) + ->mapWithKeys(function ($participant, $key) { + return [$participant->ID => $participant->User]; + }) + ->toArray(); + + return view('community.components.message.view-thread-page', [ + 'thread' => $thread, + 'messages' => $messages, + 'participants' => $participants, + 'totalPages' => $totalPages, + 'currentPage' => $currentPage, + 'canReply' => $canReply, + ]); + } + + public function pageCreate(Request $request): View + { + $toUser = $request->input('to') ?? ''; + $subject = $request->input('subject') ?? ''; + $message = $request->input('message') ?? ''; + + return view('community.components.message.new-thread-page', [ + 'toUser' => $toUser, + 'subject' => $subject, + 'message' => $message, + ]); + } + + public static function newThread(User $userFrom, User $userTo, string $title, string $body, bool $isProxied = false): MessageThread + { + $thread = new MessageThread([ + 'title' => $title, + ]); + $thread->save(); + + $participantFrom = new MessageThreadParticipant([ + 'user_id' => $userFrom->ID, + 'thread_id' => $thread->id, + ]); + + if ($isProxied) { + $participantFrom->deleted_at = Carbon::now(); + } + + $participantFrom->save(); + + if ($userTo->ID != $userFrom->ID) { + $participantTo = new MessageThreadParticipant([ + 'user_id' => $userTo->ID, + 'thread_id' => $thread->id, + ]); + + // if the recipient has blocked the sender, immediately mark the thread as deleted for the recipient + $relationship = UserRelation::getRelationship($userTo->User, $userFrom->User); + if ($relationship == UserRelationship::Blocked) { + $participantTo->deleted_at = Carbon::now(); + } + + $participantTo->save(); + } + + MessageThreadsController::addToThread($thread, $userFrom, $body); + + return $thread; + } + + public static function addToThread(MessageThread $thread, User $userFrom, string $body): void + { + $message = new Message([ + 'thread_id' => $thread->id, + 'author_id' => $userFrom->ID, + 'body' => $body, + 'created_at' => Carbon::now(), + ]); + $message->save(); + + $thread->num_messages++; + $thread->last_message_id = $message->id; + $thread->save(); + + MessageCreated::dispatch($message); + } + + public static function updateUnreadMessageCount(User $user): void + { + $totalUnread = MessageThreadParticipant::where('user_id', $user->id) + ->whereNull('deleted_at') + ->sum('num_unread'); + + $user->UnreadMessageCount = $totalUnread; + $user->save(); + } + + public static function markRead(MessageThread $thread, User $user): void + { + $participant = MessageThreadParticipant::where('user_id', $user->id) + ->where('thread_id', $thread->id) + ->whereNull('deleted_at') + ->first(); + + if ($participant) { + MessageThreadsController::markParticipantRead($participant, $user); + } + } + + private static function markParticipantRead(MessageThreadParticipant $participant, User $user): void + { + if ($participant->num_unread) { + $participant->num_unread = 0; + $participant->save(); + + MessageThreadsController::updateUnreadMessageCount($user); + } + } + + public static function deleteThread(MessageThread $thread, User $user): void + { + $participant = MessageThreadParticipant::where('thread_id', $thread->id) + ->where('user_id', $user->id) + ->first(); + + if ($participant) { + $participant->num_unread = 0; + $participant->deleted_at = Carbon::now(); + $participant->save(); + + MessageThreadsController::updateUnreadMessageCount($user); + + $hasOtherActiveParticipants = MessageThreadParticipant::where('thread_id', $thread->id) + ->where('user_id', '!=', $user->id) + ->whereNull('deleted_at') + ->exists(); + if (!$hasOtherActiveParticipants) { + // this will also cascade delete the message_participants and messages + $thread->delete(); + } + } + } +} diff --git a/app/Community/Controllers/UserMessageChainController.php b/app/Community/Controllers/UserMessageChainController.php deleted file mode 100644 index 9cb10ea7e7..0000000000 --- a/app/Community/Controllers/UserMessageChainController.php +++ /dev/null @@ -1,194 +0,0 @@ -route('chain'); - - $messageChain = UserMessageChain::firstWhere('id', $chainId); - if (!$messageChain) { - abort(404); - } - - $user = $request->user(); - if ($messageChain->sender_id != $user->id && $messageChain->recipient_id != $user->id) { - abort(404); - } - - $pageSize = 20; - $currentPage = (int) $request->input('page.number') ?? 1; - if ($currentPage < 1) { - $currentPage = 1; - } - $totalPages = (int) (($messageChain->num_messages + 19) / 20); - - if ($currentPage == $totalPages) { - // if viewing last page, mark all messages in the chain as read - UserMessageChainController::markRead($messageChain, $user); - } - - $messages = UserMessage::where('chain_id', $chainId) - ->offset(($currentPage - 1) * $pageSize) - ->limit($pageSize) - ->get(); - - return view('community.components.message.view-chain-page', [ - 'messageChain' => $messageChain, - 'messages' => $messages, - 'totalPages' => $totalPages, - 'currentPage' => $currentPage, - ]); - } - - public function pageCreate(Request $request): View - { - $toUser = $request->input('to') ?? ''; - $subject = $request->input('subject') ?? ''; - $message = $request->input('message') ?? ''; - - return view('community.components.message.new-chain-page', [ - 'toUser' => $toUser, - 'subject' => $subject, - 'message' => $message, - ]); - } - - public static function newChain(User $userFrom, User $userTo, string $title, string $body): UserMessageChain - { - $userMessageChain = new UserMessageChain([ - 'title' => $title, - 'sender_id' => $userFrom->ID, - 'recipient_id' => $userTo->ID, - ]); - - UserMessageChainController::addToChain($userMessageChain, $userFrom, $body); - return $userMessageChain; - } - - public static function addToChain(UserMessageChain $userMessageChain, User $userFrom, string $body): void - { - $now = Carbon::now(); - - $userMessageChain->num_messages++; - if ($userMessageChain->recipient_id == $userFrom->ID) { - $userMessageChain->recipient_last_post_at = $now; - - $relationship = UserRelation::getRelationship($userMessageChain->sender->User, $userFrom->User); - if ($relationship == UserRelationship::Blocked) { - $userMessageChain->sender_num_unread = 0; - $userMessageChain->sender_deleted_at = $now; - } else { - $userMessageChain->sender_num_unread++; - $userMessageChain->sender_deleted_at = null; - } - - $userTo = User::firstWhere('id', $userMessageChain->sender_id); - } else { - $userMessageChain->sender_last_post_at = $now; - - $relationship = UserRelation::getRelationship($userMessageChain->recipient->User, $userFrom->User); - if ($relationship == UserRelationship::Blocked) { - $userMessageChain->recipient_num_unread = 0; - $userMessageChain->recipient_deleted_at = $now; - } else { - $userMessageChain->recipient_num_unread++; - $userMessageChain->recipient_deleted_at = null; - } - - $userTo = User::firstWhere('id', $userMessageChain->recipient_id); - } - $userMessageChain->save(); - - $userMessage = new UserMessage([ - 'chain_id' => $userMessageChain->id, - 'author_id' => $userFrom->ID, - 'body' => $body, - ]); - $userMessage->save(); - - UserMessageChainController::updateUnreadMessageCount($userTo); - - // send email? - if (BitSet($userTo->websitePrefs, UserPreference::EmailOn_PrivateMessage) && - $relationship != UserRelationship::Blocked) { - sendPrivateMessageEmail($userTo->User, $userTo->EmailAddress, $userMessageChain->title, $body, $userFrom->User); - } - } - - private static function updateUnreadMessageCount(User $user): void - { - $unreadReplies = UserMessageChain::where('sender_id', $user->id) - ->whereNull('sender_deleted_at') - ->sum('sender_num_unread'); - - $unreadMessages = UserMessageChain::where('recipient_id', $user->id) - ->whereNull('recipient_deleted_at') - ->sum('recipient_num_unread'); - - $user->UnreadMessageCount = $unreadMessages + $unreadReplies; - $user->save(); - } - - public static function markRead(UserMessageChain $userMessageChain, User $user): void - { - if ($userMessageChain->recipient_id == $user->ID) { - if ($userMessageChain->recipient_num_unread) { - $userMessageChain->recipient_num_unread = 0; - $userMessageChain->save(); - - UserMessageChainController::updateUnreadMessageCount($user); - } - } else { - if ($userMessageChain->sender_num_unread) { - $userMessageChain->sender_num_unread = 0; - $userMessageChain->save(); - - UserMessageChainController::updateUnreadMessageCount($user); - } - } - } - - public static function deleteChain(UserMessageChain $userMessageChain, User $user): void - { - $now = Carbon::now(); - - if ($userMessageChain->recipient_id == $user->ID) { - $userMessageChain->recipient_num_unread = 0; - $userMessageChain->recipient_deleted_at = $now; - } else { - $userMessageChain->sender_num_unread = 0; - $userMessageChain->sender_deleted_at = $now; - } - - if ($userMessageChain->recipient_deleted_at != null && $userMessageChain->sender_deleted_at != null) { - // this will also cascade delete the user_messages - $userMessageChain->delete(); - } else { - $userMessageChain->save(); - } - - UserMessageChainController::updateUnreadMessageCount($user); - } -} diff --git a/app/Community/Controllers/UserMessagesController.php b/app/Community/Controllers/UserMessagesController.php deleted file mode 100644 index 4075c0cfb5..0000000000 --- a/app/Community/Controllers/UserMessagesController.php +++ /dev/null @@ -1,83 +0,0 @@ -user(); - - $respondedMessages = UserMessageChain::where('sender_id', $user->id) - ->whereNull('sender_deleted_at') - ->whereNotNull('recipient_last_post_at'); - - $receivedMessages = UserMessageChain::where('recipient_id', $user->id) - ->whereNull('recipient_deleted_at'); - - $messages = $respondedMessages->union($receivedMessages); - return $this->buildList($messages, $request, 'inbox'); - } - - private function buildList(Builder $messages, Request $request, string $mode): View - { - $totalMessages = $messages->count(); - - $pageSize = 20; - $currentPage = (int) $request->input('page.number') ?? 1; - if ($currentPage < 1) { - $currentPage = 1; - } - $totalPages = (int) (($totalMessages + 19) / 20); - - $messages = $messages - ->orderBy(DB::raw(greatestStatement(['COALESCE(recipient_last_post_at,0)', 'sender_last_post_at'])), 'DESC') - ->offset(($currentPage - 1) * $pageSize) - ->limit($pageSize) - ->get(); - - return view('community.components.message.list-page', [ - 'messages' => $messages, - 'totalPages' => $totalPages, - 'currentPage' => $currentPage, - 'unreadCount' => $request->user()->UnreadMessageCount, - 'totalMessages' => $totalMessages, - 'mode' => $mode, - ]); - } - - public function pageOutbox(Request $request): View - { - $user = $request->user(); - - $respondedMessages = UserMessageChain::where('recipient_id', $user->id) - ->whereNull('recipient_deleted_at') - ->whereRaw('recipient_last_post_at > sender_last_post_at'); - - $sentMessages = UserMessageChain::where('sender_id', $user->id) - ->whereNull('sender_deleted_at') - ->where(function ($query){ - $query->whereNull('recipient_last_post_at') - ->orWhereRaw('sender_last_post_at > recipient_last_post_at'); - }); - - $messages = $respondedMessages->union($sentMessages); - return $this->buildList($messages, $request, 'outbox'); - } -} diff --git a/app/Community/EventServiceProvider.php b/app/Community/EventServiceProvider.php index 9dfaa9626c..7460012df0 100755 --- a/app/Community/EventServiceProvider.php +++ b/app/Community/EventServiceProvider.php @@ -4,6 +4,8 @@ namespace App\Community; +use App\Community\Events\MessageCreated; +use App\Community\Listeners\NotifyMessageParticipants; use App\Community\Listeners\WriteUserActivity; use App\Platform\Events\AchievementSetBeaten; use App\Platform\Events\AchievementSetCompleted; @@ -26,6 +28,14 @@ class EventServiceProvider extends ServiceProvider WriteUserActivity::class, ], + /* + * Community Events + */ + + MessageCreated::class => [ + NotifyMessageParticipants::class, + ], + /* * Platform Events - Account Listeners */ diff --git a/app/Community/Events/MessageCreated.php b/app/Community/Events/MessageCreated.php new file mode 100644 index 0000000000..d4f60226e3 --- /dev/null +++ b/app/Community/Events/MessageCreated.php @@ -0,0 +1,28 @@ +message; + + $userFrom = User::firstWhere('ID', $message->author_id); + if (!$userFrom) { + return; + } + + $thread = MessageThread::firstWhere('id', $message->thread_id); + if (!$thread) { + return; + } + $validParticipants = 0; + $participants = MessageThreadParticipant::withTrashed()->where('thread_id', $message->thread_id)->get(); + foreach ($participants as $participant) { + if ($participant->user_id == $message->author_id) { + // don't notify the sender + continue; + } + + $userTo = User::firstWhere('ID', $participant->user_id); + if (!$userTo) { + // ignore deleted users + continue; + } + + $relationship = UserRelation::getRelationship($userTo->User, $userFrom->User); + if ($relationship == UserRelationship::Blocked) { + // ignore users who have blocked the sender + continue; + } + + $validParticipants++; + + // use direct update to avoid race condition + DB::statement("UPDATE message_thread_participants + SET num_unread = num_unread + 1, deleted_at = NULL + WHERE id = {$participant->id}"); + + MessageThreadsController::updateUnreadMessageCount($userTo); + + // send email? + if (BitSet($userTo->websitePrefs, UserPreference::EmailOn_PrivateMessage)) { + sendPrivateMessageEmail($userTo->User, $userTo->EmailAddress, $thread->title, $message->body, $userFrom->User); + } + } + } +} diff --git a/app/Community/Models/Message.php b/app/Community/Models/Message.php index 3193721748..bdd47943d0 100644 --- a/app/Community/Models/Message.php +++ b/app/Community/Models/Message.php @@ -6,34 +6,27 @@ use App\Site\Models\User; use App\Support\Database\Eloquent\BaseModel; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\SoftDeletes; use Laravel\Scout\Searchable; class Message extends BaseModel { - use SoftDeletes; use Searchable; - // TODO rename Message table to messages - // TODO rename ID column to id - // TODO drop UserTo, migrate to recipient_id - // TODO drop UserFrom, migrate to sender_id - // TODO rename Title column to title - // TODO rename Payload column to body - // TODO rename TimeSent column to sent_at - // TODO drop Unread, migrate to read_at - // TODO drop Type - protected $table = 'Messages'; + protected $table = 'messages'; - protected $primaryKey = 'ID'; - - public const CREATED_AT = 'TimeSent'; + public const CREATED_AT = 'created_at'; public const UPDATED_AT = null; + protected $fillable = [ + 'thread_id', + 'author_id', + 'body', + 'created_at', + ]; + protected $casts = [ - 'read_at' => 'datetime', + 'created_at' => 'datetime', ]; // == search @@ -60,29 +53,12 @@ public function shouldBeSearchable(): bool // == relations /** - * @return BelongsTo + * @return BelongsTo */ - public function sender(): BelongsTo + public function author(): BelongsTo { - return $this->belongsTo(User::class, 'sender_id'); - } - - /** - * @return BelongsTo - */ - public function recipient(): BelongsTo - { - return $this->belongsTo(User::class, 'recipient_id'); + return $this->belongsTo(User::class, 'author_id'); } // == scopes - - /** - * @param Builder $query - * @return Builder - */ - public function scopeUnread(Builder $query): Builder - { - return $query->whereNull('read_at'); - } } diff --git a/app/Community/Models/MessageThread.php b/app/Community/Models/MessageThread.php new file mode 100644 index 0000000000..43e2a4e624 --- /dev/null +++ b/app/Community/Models/MessageThread.php @@ -0,0 +1,27 @@ + 'datetime', - ]; - - // == accessors - - // == mutators - - // == relations - - /** - * @return BelongsTo - */ - public function author(): BelongsTo - { - return $this->belongsTo(User::class, 'author_id'); - } - - // == scopes -} diff --git a/app/Community/Models/UserMessageChain.php b/app/Community/Models/UserMessageChain.php deleted file mode 100644 index d9ca95932f..0000000000 --- a/app/Community/Models/UserMessageChain.php +++ /dev/null @@ -1,57 +0,0 @@ - 'datetime', - 'recipient_deleted_at' => 'datetime', - 'sender_last_post_at' => 'datetime', - 'recipient_last_post_at' => 'datetime', - ]; - - // == accessors - - // == mutators - - // == relations - - /** - * @return BelongsTo - */ - public function sender(): BelongsTo - { - return $this->belongsTo(User::class, 'sender_id'); - } - - /** - * @return BelongsTo - */ - public function recipient(): BelongsTo - { - return $this->belongsTo(User::class, 'recipient_id'); - } - - // == scopes -} diff --git a/app/Community/Models/UserRelation.php b/app/Community/Models/UserRelation.php index 9a3b31c693..84ca29522d 100644 --- a/app/Community/Models/UserRelation.php +++ b/app/Community/Models/UserRelation.php @@ -33,6 +33,7 @@ public static function getRelationship(string $user, string $relatedUser): int $relation = UserRelation::where('User', $user) ->where('Friend', $relatedUser) ->first(); + return $relation ? $relation->Friendship : UserRelationship::NotFollowing; } diff --git a/app/Community/RouteServiceProvider.php b/app/Community/RouteServiceProvider.php index bcd3f27c26..0ada8a5c8e 100755 --- a/app/Community/RouteServiceProvider.php +++ b/app/Community/RouteServiceProvider.php @@ -5,8 +5,7 @@ namespace App\Community; use App\Community\Controllers\ContactController; -use App\Community\Controllers\UserMessageChainController; -use App\Community\Controllers\UserMessagesController; +use App\Community\Controllers\MessageThreadsController; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Route; @@ -231,12 +230,11 @@ protected function mapWebRoutes(): void // // Route::get('history', [PlayerHistoryController::class, 'index'])->name('history.index'); /* - * inbox + * messages */ - Route::get('message/new', [UserMessageChainController::class, 'pageCreate'])->name('message.new'); - Route::get('message/{chain}', UserMessageChainController::class)->name('message.view-chain'); - Route::get('messages', UserMessagesController::class)->name('message.inbox'); - Route::get('messages/outbox', [UserMessagesController::class, 'pageOutbox'])->name('message.outbox'); + Route::get('message/new', [MessageThreadsController::class, 'pageCreate'])->name('message.new'); + Route::get('message/{thread_id}', [MessageThreadsController::class, 'pageView'])->name('message.view'); + Route::get('messages', [MessageThreadsController::class, 'pageList'])->name('message.list'); // /* // * tickets diff --git a/app/Helpers/database/ticket.php b/app/Helpers/database/ticket.php index 6614b6bff8..67114cac6b 100644 --- a/app/Helpers/database/ticket.php +++ b/app/Helpers/database/ticket.php @@ -1,6 +1,6 @@ save(); + MessageThreadsController::newThread($user, $author, "Bug Report ($gameTitle)", + $bugReportMessage, isProxied: true); // don't put the message in the user's message list unless the recipient replies } // notify subscribers other than the achievement's author diff --git a/app/Helpers/database/user-auth.php b/app/Helpers/database/user-auth.php index 37d4dc07d2..7176d6ff73 100644 --- a/app/Helpers/database/user-auth.php +++ b/app/Helpers/database/user-auth.php @@ -85,7 +85,7 @@ function authenticateForConnect(?string $username, ?string $pass = null, ?string 'Token' => $user->appToken, 'Score' => $user->RAPoints, 'SoftcoreScore' => $user->RASoftcorePoints, - 'Messages' => $user->UnreadMessageCount, + 'Messages' => $user->UnreadMessageCount ?? 0, 'Permissions' => $permissions, 'AccountType' => Permissions::toString($permissions), ]; diff --git a/app/Helpers/util/mail.php b/app/Helpers/util/mail.php index 7df03f40db..0a994f0c1b 100644 --- a/app/Helpers/util/mail.php +++ b/app/Helpers/util/mail.php @@ -363,7 +363,7 @@ function SendPrivateMessageEmail( // Also used for Generic text: $emailTitle = "New Private Message from $fromUser"; - $link = "here"; + $link = "here"; $msg = "Hello $user!
          " . "You have received a new private message from $fromUser.

          " . diff --git a/app/Site/Components/NotificationIcon.php b/app/Site/Components/NotificationIcon.php index 42f8be04a3..e97a7dde22 100644 --- a/app/Site/Components/NotificationIcon.php +++ b/app/Site/Components/NotificationIcon.php @@ -24,7 +24,7 @@ public function render(): View if ($user->unread_messages_count) { $notifications->push([ - 'link' => route('message.inbox'), + 'link' => route('message.list'), 'title' => $user->unread_messages_count . ' ' . __res('message', (int) $user->unread_messages_count), ]); } diff --git a/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php b/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php deleted file mode 100644 index 4f59d23729..0000000000 --- a/database/migrations/platform/2023_11_22_000000_create_user_message_chains_table.php +++ /dev/null @@ -1,37 +0,0 @@ -bigIncrements('id'); - $table->string('title'); - $table->unsignedBigInteger('sender_id')->nullable(); - $table->unsignedBigInteger('recipient_id')->nullable(); - $table->integer('num_messages')->default(0); - $table->integer('sender_num_unread')->default(0); - $table->integer('recipient_num_unread')->default(0); - $table->timestampTz('sender_last_post_at')->nullable(); - $table->timestampTz('recipient_last_post_at')->nullable(); - $table->timestampTz('sender_deleted_at')->nullable(); - $table->timestampTz('recipient_deleted_at')->nullable(); - - $table->index('sender_id'); - $table->index('recipient_id'); - - $table->foreign('sender_id')->references('ID')->on('UserAccounts')->onDelete('set null'); - $table->foreign('recipient_id')->references('ID')->on('UserAccounts')->onDelete('set null'); - }); - } - - public function down(): void - { - Schema::dropIfExists('user_message_chains'); - } -}; diff --git a/database/migrations/platform/2023_11_22_000001_create_user_messages_table.php b/database/migrations/platform/2023_11_22_000001_create_user_messages_table.php deleted file mode 100644 index f834440a68..0000000000 --- a/database/migrations/platform/2023_11_22_000001_create_user_messages_table.php +++ /dev/null @@ -1,38 +0,0 @@ -bigIncrements('id'); - $table->unsignedBigInteger('chain_id'); - $table->unsignedBigInteger('author_id'); - $table->text('body'); - $table->timestampTz('created_at')->useCurrent(); - - $table->index('chain_id'); - - $table->foreign('chain_id')->references('ID')->on('user_message_chains')->onDelete('cascade'); - $table->foreign('author_id')->references('ID')->on('UserAccounts')->onDelete('cascade'); - }); - - Schema::table('Messages', function (Blueprint $table) { - $table->unsignedBigInteger('migrated_id')->nullable(); - }); - } - - public function down(): void - { - Schema::table('Messages', function (Blueprint $table) { - $table->dropColumn('migrated_id'); - }); - - Schema::dropIfExists('user_messages'); - } -}; diff --git a/database/migrations/platform/2023_11_25_000000_create_message_threads_table.php b/database/migrations/platform/2023_11_25_000000_create_message_threads_table.php new file mode 100644 index 0000000000..fa3b255121 --- /dev/null +++ b/database/migrations/platform/2023_11_25_000000_create_message_threads_table.php @@ -0,0 +1,24 @@ +bigIncrements('id'); + $table->string('title'); + $table->integer('num_messages')->default(0); + $table->unsignedBigInteger('last_message_id')->nullable(); + }); + } + + public function down(): void + { + Schema::dropIfExists('message_threads'); + } +}; diff --git a/database/migrations/platform/2023_11_25_000001_create_message_thread_participants.php b/database/migrations/platform/2023_11_25_000001_create_message_thread_participants.php new file mode 100644 index 0000000000..56c2d26b3b --- /dev/null +++ b/database/migrations/platform/2023_11_25_000001_create_message_thread_participants.php @@ -0,0 +1,28 @@ +bigIncrements('id'); + $table->unsignedBigInteger('thread_id'); + $table->unsignedBigInteger('user_id'); + $table->integer('num_unread')->default(0); + $table->timestampTz('deleted_at')->nullable(); + + $table->foreign('user_id')->references('ID')->on('UserAccounts')->onDelete('cascade'); + $table->foreign('thread_id')->references('ID')->on('message_threads')->onDelete('cascade'); + }); + } + + public function down(): void + { + Schema::dropIfExists('message_thread_participants'); + } +}; diff --git a/database/migrations/platform/2023_11_25_000002_update_messages_table.php b/database/migrations/platform/2023_11_25_000002_update_messages_table.php new file mode 100644 index 0000000000..626203f674 --- /dev/null +++ b/database/migrations/platform/2023_11_25_000002_update_messages_table.php @@ -0,0 +1,117 @@ +getDriverName() === 'sqlite') { + // just recreate the table for the unit tests + Schema::dropIfExists('Messages'); + + Schema::create('messages', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->unsignedBigInteger('thread_id'); + $table->unsignedBigInteger('author_id'); + $table->text('body')->nullable(); + $table->timestampTz('created_at')->nullable(); + + $table->foreign('author_id')->references('ID')->on('UserAccounts')->onDelete('cascade'); + $table->foreign('thread_id')->references('ID')->on('message_threads')->onDelete('cascade'); + }); + + return; + } + + Schema::table('Messages', function (Blueprint $table) { + $table->dropForeign('messages_recipient_id_foreign'); + $table->dropForeign('messages_sender_id_foreign'); + + $table->dropColumn(['recipient_id', + 'sender_id', + 'read_at', + 'Type', + 'recipient_deleted_at', + 'sender_deleted_at', + 'deleted_at']); + + $table->unsignedBigInteger('thread_id')->after('ID'); + $table->unsignedBigInteger('author_id')->after('thread_id'); + + $table->renameColumn('Payload', 'body'); + $table->renameColumn('TimeSent', 'created_at'); + $table->renameColumn('ID', 'id'); + }); + + // populate author_id so we can add a foreign key + DB::statement("UPDATE Messages m SET m.author_id = (SELECT u.ID FROM UserAccounts u WHERE u.User = m.UserFrom)"); + + // delete records associated to non-existant users + DB::statement("DELETE FROM Messages WHERE author_id=0"); + + // add the foreign key + Schema::table('Messages', function (Blueprint $table) { + $table->foreign('author_id')->references('ID')->on('UserAccounts')->onDelete('cascade'); + }); + + Schema::rename('Messages', 'messages_'); + Schema::rename('messages_', 'messages'); + + // cannot add the thread_id foreign key if there are any un-sync'd + // records. the `ra:sync:messages` command will add it when it's done. + $unsyncedCount = Message::where('thread_id', 0)->count(); + if ($unsyncedCount == 0) { + Schema::table('messages', function (Blueprint $table) { + $table->foreign('thread_id')->references('ID')->on('message_threads')->onDelete('cascade'); + }); + } + } + + public function down(): void + { + Schema::rename('messages', 'messages_'); + Schema::rename('messages_', 'Messages'); + + Schema::table('Messages', function (Blueprint $table) { + $sm = Schema::getConnection()->getDoctrineSchemaManager(); + $foreignKeysFound = $sm->listTableForeignKeys('Messages'); + + foreach ($foreignKeysFound as $foreignKey) { + if ($foreignKey->getName() == 'messages_thread_id_foreign' + || $foreignKey->getName() == 'messages_author_id_foreign') { + $table->dropForeign($foreignKey->getName()); + } + } + + $table->dropColumn('thread_id'); + $table->dropColumn('author_id'); + + $table->renameColumn('body', 'Payload'); + $table->renameColumn('created_at', 'TimeSent'); + + $table->renameColumn('id', 'ID'); + + $table->unsignedBigInteger('recipient_id')->nullable()->after('ID'); + $table->unsignedBigInteger('sender_id')->nullable()->after('recipient_id'); + + $table->timestampTz('read_at')->nullable()->before('Unread'); + + $table->unsignedInteger('Type')->nullable()->after('Unread'); + + $table->timestamp('recipient_deleted_at')->nullable(); + $table->timestamp('sender_deleted_at')->nullable(); + + $table->softDeletesTz(); + + $table->index('read_at'); + + $table->foreign('recipient_id', 'messages_recipient_id_foreign')->references('ID')->on('UserAccounts')->onDelete('set null'); + $table->foreign('sender_id', 'messages_sender_id_foreign')->references('ID')->on('UserAccounts')->onDelete('set null'); + }); + } +}; diff --git a/public/managehashes.php b/public/managehashes.php index 6483ab0004..a79be99df7 100644 --- a/public/managehashes.php +++ b/public/managehashes.php @@ -113,7 +113,7 @@ function UnlinkHash(user, gameID, hash, elem) { } echo "
"; - echo "
Warning: PLEASE be careful with this tool. If in doubt, leave a message for admins and they'll help sort it.
"; + echo "
Warning: PLEASE be careful with this tool. If in doubt, leave a message for admins and they'll help sort it.
"; echo "
Currently this game has $numLinks unique hashes registered for it:
"; diff --git a/public/request/message/create.php b/public/request/message/create.php index a5fc11b390..e78104d0dc 100644 --- a/public/request/message/create.php +++ b/public/request/message/create.php @@ -1,12 +1,11 @@ user(); $input = Validator::validate(Arr::wrap(request()->post()), [ - 'chain' => 'nullable|integer', + 'thread_id' => 'nullable|integer', 'body' => 'required|string|max:60000', - 'title' => 'required_without:chain|string|max:255', - 'recipient' => 'required_without:chain|exists:UserAccounts,User', + 'title' => 'required_without:thread_id|string|max:255', + 'recipient' => 'required_without:thread_id|exists:UserAccounts,User', ]); -if (array_key_exists('chain', $input) && $input['chain'] != null) { - $userMessageChain = UserMessageChain::firstWhere('id', $input['chain']); - if (!$userMessageChain) { +if (array_key_exists('thread_id', $input) && $input['thread_id'] != null) { + $thread = MessageThread::firstWhere('id', $input['thread_id']); + if (!$thread) { return back()->withErrors(__('legacy.error.error')); } - if ($userMessageChain->recipient_id != $user->ID && $userMessageChain->sender_id != $user->ID) { + $participant = MessageThreadParticipant::where('thread_id', $input['thread_id']) + ->where('user_id', $user->ID); + if (!$participant->exists()) { return back()->withErrors(__('legacy.error.error')); } - UserMessageChainController::addToChain($userMessageChain, $user, $input['body']); - return redirect(route("message.view-chain", $userMessageChain->id)); + MessageThreadsController::addToThread($thread, $user, $input['body']); +} else { + $recipient = User::firstWhere('User', $input['recipient']); + $thread = MessageThreadsController::newThread($user, $recipient, $input['title'], $input['body']); } -$recipient = User::firstWhere('User', $input['recipient']); -$userMessageChain = UserMessageChainController::newChain($user, $recipient, $input['title'], $input['body']); -return redirect(route("message.outbox"))->with('success', __('legacy.success.message_send')); +return redirect(route("message.view", $thread->id))->with('success', __('legacy.success.message_send')); diff --git a/public/request/message/delete.php b/public/request/message/delete.php index cf3a262da9..892905847d 100644 --- a/public/request/message/delete.php +++ b/public/request/message/delete.php @@ -1,7 +1,8 @@ post()), [ - 'chain' => 'required|integer|exists:user_message_chains,id', + 'thread_id' => 'required|integer|exists:message_threads,id', ]); +$thread = MessageThread::firstWhere('id', $input['thread_id']); +if (!$thread) { + return back()->withErrors(__('legacy.error.error')); +} + /** @var User $user */ $user = request()->user(); -$userMessageChain = UserMessageChain::firstWhere('id', $input['chain']); -if (!$userMessageChain) { - return back()->withErrors(__('legacy.error.error')); -} -if ($userMessageChain->recipient_id != $user->ID && $userMessageChain->sender_id != $user->ID) { +$participating = MessageThreadParticipant::where('thread_id', $input['thread_id']) + ->where('user_id', $user->ID) + ->exists(); + +if (!$participating) { return back()->withErrors(__('legacy.error.error')); } -UserMessageChainController::deleteChain($userMessageChain, $user); +MessageThreadsController::deleteThread($thread, $user); -response()->json(['message' => __('legacy.success.ok')]); +return redirect(route("message.list"))->with('success', __('legacy.success.message_delete')); diff --git a/resources/views/community/components/message/breadcrumbs.blade.php b/resources/views/community/components/message/breadcrumbs.blade.php deleted file mode 100644 index e259aa5ea1..0000000000 --- a/resources/views/community/components/message/breadcrumbs.blade.php +++ /dev/null @@ -1,10 +0,0 @@ -@props([ - 'targetUsername' => '', - 'currentPage' => '', -]) - - diff --git a/resources/views/community/components/message/icon.blade.php b/resources/views/community/components/message/icon.blade.php index 84ff210c0f..79b8e6f3f1 100644 --- a/resources/views/community/components/message/icon.blade.php +++ b/resources/views/community/components/message/icon.blade.php @@ -1,5 +1,5 @@