Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Threaded messages #2062

Merged
merged 48 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b8fd943
add migration steps
Jamiras Nov 22, 2023
4af4d3c
add sync command
Jamiras Nov 22, 2023
095f543
add message viewer
Jamiras Nov 22, 2023
1553085
add inbox
Jamiras Nov 22, 2023
1a36a0d
add outbox
Jamiras Nov 22, 2023
4752ffa
support replying to messages
Jamiras Nov 23, 2023
da2dce6
support creating new messages
Jamiras Nov 23, 2023
5c5aebc
hook up delete
Jamiras Nov 23, 2023
45fccb7
automatically delete messages from blocked users
Jamiras Nov 23, 2023
7243944
support for deleted users
Jamiras Nov 23, 2023
a96e9c1
update UnreadMessageCount
Jamiras Nov 24, 2023
c2419ec
send private message emails
Jamiras Nov 24, 2023
84d1c8a
eliminate inbox.php
Jamiras Nov 24, 2023
0213317
eliminate createmessage.php
Jamiras Nov 24, 2023
f663c6d
eliminate database/message.php
Jamiras Nov 24, 2023
3d5f7d6
hard-delete message when both users delete it
Jamiras Nov 24, 2023
27936f0
rename tables
Jamiras Nov 26, 2023
3713e15
phpstan
Jamiras Nov 27, 2023
fdb7dd1
rename table
Jamiras Nov 27, 2023
4e06d44
address feedback
Jamiras Nov 28, 2023
e38f391
move author populating from migrate step to sync command
Jamiras Nov 28, 2023
c932cba
rename NotifyMessageParticipants -> NotifyMessageThreadParticipants
Jamiras Nov 28, 2023
bf66891
elimininate redunant naming in models
Jamiras Nov 28, 2023
f19f1d7
add created/updated timestamps to threads and participants
Jamiras Nov 28, 2023
a7f31c5
add isBlocking helper function to ActsAsCommunityMember
Jamiras Nov 28, 2023
5e75140
migrate deleteThread to action
Jamiras Nov 28, 2023
f458100
migrate markRead to action
Jamiras Nov 28, 2023
bd6dbb3
move addToThread to Action
Jamiras Nov 28, 2023
97d4578
migrate newThread to action
Jamiras Nov 28, 2023
c2c15fc
use implicit soft-delete for participant thread deletion
Jamiras Nov 28, 2023
4c359e2
use default laravel routes
Jamiras Nov 29, 2023
cc1403d
move create.php -> controller.store
Jamiras Nov 29, 2023
8f9f087
move delete.php -> controller.destroy
Jamiras Nov 29, 2023
54e60e5
use __res for resource lookup
Jamiras Nov 29, 2023
8ff401a
use action to delete thread participation on user deletion
Jamiras Nov 29, 2023
c7a6ac6
composer fix
Jamiras Nov 29, 2023
6838927
rename SyncMessages -> MigrateMessages; use chunkById
Jamiras Dec 1, 2023
8fc03fc
restore redirect for createmessage.php
Jamiras Dec 1, 2023
3c89ad7
rename MessageThreadsController -> MessageThreadController
Jamiras Dec 1, 2023
cbdc889
restore MessagePolicy
Jamiras Dec 1, 2023
fecde60
update variable names
Jamiras Dec 1, 2023
39b0b91
composer fix
Jamiras Dec 1, 2023
0ded67d
update routes
Jamiras Dec 1, 2023
f185186
handle message to self
Jamiras Dec 1, 2023
8faae59
composer fix
Jamiras Dec 1, 2023
8d3ac88
allow framework to locate model
Jamiras Dec 2, 2023
80d4a63
remove breadcrumb and user info from title
Jamiras Dec 3, 2023
858ac95
Merge branch 'master' into threaded_messages
Jamiras Dec 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions app/Community/Actions/AddToMessageThreadAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace App\Community\Actions;

use App\Community\Events\MessageCreated;
use App\Community\Models\Message;
use App\Community\Models\MessageThread;
use App\Site\Models\User;
use Illuminate\Support\Carbon;

class AddToMessageThreadAction
{
public function execute(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);
}
}
50 changes: 50 additions & 0 deletions app/Community/Actions/CreateMessageThreadAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace App\Community\Actions;

use App\Community\Models\MessageThread;
use App\Community\Models\MessageThreadParticipant;
use App\Site\Models\User;
use Illuminate\Support\Carbon;

class CreateMessageThreadAction
{
public function execute(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
if ($userTo->isBlocking($userFrom->User)) {
$participantTo->deleted_at = Carbon::now();
}

$participantTo->save();
}

(new AddToMessageThreadAction())->execute($thread, $userFrom, $body);

return $thread;
}
}
40 changes: 40 additions & 0 deletions app/Community/Actions/DeleteMessageThreadAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace App\Community\Actions;

use App\Community\Models\MessageThread;
use App\Community\Models\MessageThreadParticipant;
use App\Site\Models\User;

class DeleteMessageThreadAction
{
public function execute(MessageThread $thread, User $user): void
{
$participant = MessageThreadParticipant::where('thread_id', $thread->id)
->where('user_id', $user->id)
->first();

if ($participant) {
// make sure num_unread is 0 before we soft-delete the record.
if ($participant->num_unread) {
$participant->num_unread = 0;
$participant->save();
}

$participant->delete();

(new UpdateUnreadMessageCountAction())->execute($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();
}
}
}
}
34 changes: 34 additions & 0 deletions app/Community/Actions/ReadMessageThreadAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace App\Community\Actions;

use App\Community\Models\MessageThread;
use App\Community\Models\MessageThreadParticipant;
use App\Site\Models\User;

class ReadMessageThreadAction
{
public function execute(MessageThread $thread, User $user): void
{
$participant = MessageThreadParticipant::where('user_id', $user->id)
->where('thread_id', $thread->id)
->whereNull('deleted_at')
->first();

if ($participant) {
ReadMessageThreadAction::markParticipantRead($participant, $user);
}
}

public static function markParticipantRead(MessageThreadParticipant $participant, User $user): void
{
if ($participant->num_unread) {
$participant->num_unread = 0;
$participant->save();

(new UpdateUnreadMessageCountAction())->execute($user);
}
}
}
21 changes: 21 additions & 0 deletions app/Community/Actions/UpdateUnreadMessageCountAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Community\Actions;

use App\Community\Models\MessageThreadParticipant;
use App\Site\Models\User;

class UpdateUnreadMessageCountAction
{
public function execute(User $user): void
{
$totalUnread = MessageThreadParticipant::where('user_id', $user->id)
->whereNull('deleted_at')
->sum('num_unread');

$user->UnreadMessageCount = $totalUnread;
$user->save();
}
}
4 changes: 2 additions & 2 deletions app/Community/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

namespace App\Community;

use App\Community\Commands\MigrateMessages;
use App\Community\Commands\SyncComments;
use App\Community\Commands\SyncForumCategories;
use App\Community\Commands\SyncForums;
use App\Community\Commands\SyncForumTopics;
use App\Community\Commands\SyncMessages;
use App\Community\Commands\SyncNews;
use App\Community\Commands\SyncRatings;
use App\Community\Commands\SyncTickets;
Expand Down Expand Up @@ -54,11 +54,11 @@ public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->commands([
MigrateMessages::class,
SyncComments::class,
SyncForumCategories::class,
SyncForums::class,
SyncForumTopics::class,
SyncMessages::class,
SyncNews::class,
SyncRatings::class,
SyncTickets::class,
Expand Down
3 changes: 3 additions & 0 deletions app/Community/AuthServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use App\Community\Models\ForumTopicComment;
use App\Community\Models\GameComment;
use App\Community\Models\Message;
use App\Community\Models\MessageThread;
use App\Community\Models\News;
use App\Community\Models\NewsComment;
use App\Community\Models\TriggerTicket;
Expand All @@ -27,6 +28,7 @@
use App\Community\Policies\ForumTopicPolicy;
use App\Community\Policies\GameCommentPolicy;
use App\Community\Policies\MessagePolicy;
use App\Community\Policies\MessageThreadPolicy;
use App\Community\Policies\NewsCommentPolicy;
use App\Community\Policies\NewsPolicy;
use App\Community\Policies\TriggerTicketPolicy;
Expand All @@ -46,6 +48,7 @@ class AuthServiceProvider extends ServiceProvider
ForumTopic::class => ForumTopicPolicy::class,
GameComment::class => GameCommentPolicy::class,
Message::class => MessagePolicy::class,
luchaos marked this conversation as resolved.
Show resolved Hide resolved
MessageThread::class => MessageThreadPolicy::class,
News::class => NewsPolicy::class,
NewsComment::class => NewsCommentPolicy::class,
TriggerTicket::class => TriggerTicketPolicy::class,
Expand Down
157 changes: 157 additions & 0 deletions app/Community/Commands/MigrateMessages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

declare(strict_types=1);

namespace App\Community\Commands;

use App\Community\Models\Message;
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 MigrateMessages extends Command
{
protected $signature = 'ra:platform:messages:migrate-to-threads';
protected $description = 'Sync messages';

public function __construct()
{
parent::__construct();
}

public function handle(): void
{
$count = Message::where('thread_id', 0)->count();

$progressBar = $this->output->createProgressBar($count);
$progressBar->start();

// populate author_id for all records
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");

// process remaining unprocessed records (thread_id=0)
// have to do this in batches to prevent exhausting memory
// due to requesting payloads (message content)
Message::where('thread_id', 0)->chunkById(100, function ($messages) use ($progressBar) {
foreach ($messages as $message) {
$this->migrateMessage($message);
$progressBar->advance();
}
});

$count = Message::where('thread_id', 0)->count();
if ($count == 0) {
// all messages sync'd. add the foreign keys so deletes will cascade
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$foreignKeysFound = $sm->listTableForeignKeys('messages');

$foundThreadForeignKey = false;
$foundAuthorForeignKey = false;
foreach ($foreignKeysFound as $foreignKey) {
if ($foreignKey->getName() == 'messages_thread_id_foreign') {
$foundThreadForeignKey = true;
} elseif ($foreignKey->getName() == 'messages_author_id_foreign') {
$foundAuthorForeignKey = true;
}
}

if (!$foundThreadForeignKey) {
Schema::table('messages', function (Blueprint $table) {
$table->foreign('thread_id')->references('ID')->on('message_threads')->onDelete('cascade');
});
}

if (!$foundAuthorForeignKey) {
Schema::table('messages', function (Blueprint $table) {
$table->foreign('author_id')->references('ID')->on('UserAccounts')->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=mtp.updated_at
WHERE mt.num_messages=1 AND mt.title LIKE 'Bug Report (%'");

$progressBar->finish();
$this->line(PHP_EOL);
}

private function migrateMessage(Message $message): void
{
// 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: ') {
$threadId = Message::where('title', '=', substr($message->Title, 4))
->where('UserFrom', '=', $message->UserTo)
->where('UserTo', '=', $message->UserFrom)
->where('id', '<', $message->id)
->value('thread_id');
if ($threadId > 0) {
$thread = MessageThread::firstWhere('id', $threadId);
}
}

if ($thread === null) {
$thread = new MessageThread([
'title' => $message->Title,
'created_at' => $message->created_at,
'updated_at' => $message->created_at,
]);
$thread->save();

$participantTo = new MessageThreadParticipant([
'user_id' => $userTo->ID,
'thread_id' => $thread->id,
'created_at' => $message->created_at,
'updated_at' => $message->created_at,
]);
$participantTo->save();

if ($message->author_id != $userTo->ID) {
$participantFrom = new MessageThreadParticipant([
'user_id' => $message->author_id,
'thread_id' => $thread->id,
'created_at' => $message->created_at,
'updated_at' => $message->created_at,
]);
$participantFrom->save();
}
} else {
$threadParticipants = MessageThreadParticipant::withTrashed()->where('thread_id', $thread->id);
$participantTo = $threadParticipants->where('user_id', $userTo->ID)->first();
}

if ($message->Unread) {
$participantTo->num_unread++;
$participantTo->save();
}

$thread->num_messages++;
$thread->last_message_id = $message->id;
$thread->updated_at = $message->created_at;
$thread->timestamps = false;
$thread->save();

$message->thread_id = $thread->id;
$message->save();
}
}
Loading
Loading