From 3e0263decd1850932e48ff55ffffefba69cfe080 Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 17 Dec 2022 18:10:58 +0100 Subject: [PATCH 01/22] new schema --- app/Models/Conversation.php | 26 ++++++++++++ app/Models/{DirectMessage.php => Message.php} | 6 +-- app/Models/User.php | 15 ++++++- ...2_16_221527_create_conversations_table.php | 37 ++++++++++++++++++ ..._221627_create_conversation_user_table.php | 34 ++++++++++++++++ ...22_12_16_221727_create_messages_table.php} | 8 ++-- group_messaging_db.png | Bin 37832 -> 37423 bytes 7 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 app/Models/Conversation.php rename app/Models/{DirectMessage.php => Message.php} (95%) create mode 100644 database/migrations/2022_12_16_221527_create_conversations_table.php create mode 100644 database/migrations/2022_12_16_221627_create_conversation_user_table.php rename database/migrations/{2022_11_25_184509_create_direct_messages_table.php => 2022_12_16_221727_create_messages_table.php} (71%) diff --git a/app/Models/Conversation.php b/app/Models/Conversation.php new file mode 100644 index 0000000..5d7ca14 --- /dev/null +++ b/app/Models/Conversation.php @@ -0,0 +1,26 @@ +belongsToMany(User::class)->withTimestamps(); + } + + public function messages() + { + return $this->hasMany(Message::class); + } +} diff --git a/app/Models/DirectMessage.php b/app/Models/Message.php similarity index 95% rename from app/Models/DirectMessage.php rename to app/Models/Message.php index d4fc73f..3d85888 100644 --- a/app/Models/DirectMessage.php +++ b/app/Models/Message.php @@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -class DirectMessage extends Model +class Message extends Model { use BroadcastsEvents, HasFactory; @@ -26,9 +26,9 @@ public function user(): BelongsTo return $this->belongsTo(User::class); } - public function targetUser(): BelongsTo + public function conversation(): BelongsTo { - return $this->belongsTo(User::class, 'target_user_id'); + return $this->belongsTo(Conversation::class); } // -------------------------------------------- diff --git a/app/Models/User.php b/app/Models/User.php index 6173b67..16680df 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,6 +4,8 @@ // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -42,8 +44,17 @@ class User extends Authenticatable 'email_verified_at' => 'datetime', ]; - public function directMessages() + // -------------------------------------------- + // Relations + // -------------------------------------------- + + public function conversations(): BelongsToMany + { + return $this->belongsToMany(Conversation::class)->withTimestamps(); + } + + public function messages(): HasMany { - $this->hasMany(DirectMessage::class); + return $this->hasMany(Message::class); } } diff --git a/database/migrations/2022_12_16_221527_create_conversations_table.php b/database/migrations/2022_12_16_221527_create_conversations_table.php new file mode 100644 index 0000000..ebeb41f --- /dev/null +++ b/database/migrations/2022_12_16_221527_create_conversations_table.php @@ -0,0 +1,37 @@ +id(); + $table->timestamps(); + + $table->string('name'); + + $table->string('type'); + + $table->string('visibility'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('conversations'); + } +}; diff --git a/database/migrations/2022_12_16_221627_create_conversation_user_table.php b/database/migrations/2022_12_16_221627_create_conversation_user_table.php new file mode 100644 index 0000000..e4279c4 --- /dev/null +++ b/database/migrations/2022_12_16_221627_create_conversation_user_table.php @@ -0,0 +1,34 @@ +timestamps(); + + $table->foreignId('user_id')->constrained(); + + $table->foreignId('conversation_id')->constrained(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('conversation_user'); + } +}; diff --git a/database/migrations/2022_11_25_184509_create_direct_messages_table.php b/database/migrations/2022_12_16_221727_create_messages_table.php similarity index 71% rename from database/migrations/2022_11_25_184509_create_direct_messages_table.php rename to database/migrations/2022_12_16_221727_create_messages_table.php index 33eaa7d..ce64000 100644 --- a/database/migrations/2022_11_25_184509_create_direct_messages_table.php +++ b/database/migrations/2022_12_16_221727_create_messages_table.php @@ -8,21 +8,21 @@ { public function up(): void { - Schema::create('direct_messages', function (Blueprint $table) { + Schema::create('messages', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->text('content'); - $table->unsignedBigInteger('target_user_id'); $table->foreignId('user_id')->constrained(); - $table->foreign('target_user_id')->references('id')->on('users'); + $table->unsignedBigInteger('target_user_id'); + $table->foreignId('conversation_id')->constrained(); }); } public function down(): void { - Schema::dropIfExists('direct_messages'); + Schema::dropIfExists('messages'); } }; diff --git a/group_messaging_db.png b/group_messaging_db.png index 19bf17f0ba5741b3271f5f9727be2446215b1f68..67d361b125f9b7f216c2bf6d2a11647091e01eac 100644 GIT binary patch literal 37423 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 literal 37832 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 H;j8}xt1=7{ From 29826c09a5d108f26cd1bebcd393e36b01caf6ba Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 17 Dec 2022 19:03:58 +0100 Subject: [PATCH 02/22] fix conversations table --- app/Models/Conversation.php | 2 ++ database/factories/ConversationFactory.php | 16 ++++++++++++++++ ...2_12_16_221527_create_conversations_table.php | 6 +++--- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 database/factories/ConversationFactory.php diff --git a/app/Models/Conversation.php b/app/Models/Conversation.php index 5d7ca14..8a6d09f 100644 --- a/app/Models/Conversation.php +++ b/app/Models/Conversation.php @@ -10,6 +10,8 @@ class Conversation extends Model { use HasFactory; + protected $guarded = ['id']; + // -------------------------------------------- // Relations // -------------------------------------------- diff --git a/database/factories/ConversationFactory.php b/database/factories/ConversationFactory.php new file mode 100644 index 0000000..8304714 --- /dev/null +++ b/database/factories/ConversationFactory.php @@ -0,0 +1,16 @@ + + */ + public function definition() + { + return []; + } +} diff --git a/database/migrations/2022_12_16_221527_create_conversations_table.php b/database/migrations/2022_12_16_221527_create_conversations_table.php index ebeb41f..0e4b2cc 100644 --- a/database/migrations/2022_12_16_221527_create_conversations_table.php +++ b/database/migrations/2022_12_16_221527_create_conversations_table.php @@ -17,11 +17,11 @@ public function up() $table->id(); $table->timestamps(); - $table->string('name'); + $table->enum('type', ['direct', 'group']); - $table->string('type'); + $table->string('name')->nullable(); - $table->string('visibility'); + $table->string('visibility')->nullable(); }); } From f4636d5b47fc08c1ee0f37e5a6b84802be2bea43 Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 17 Dec 2022 19:10:20 +0100 Subject: [PATCH 03/22] createDirectConversation --- .../UserConversationController.php | 24 ++++++++++++++ routes/api.php | 4 +++ tests/Feature/ExampleTest.php | 21 ------------ .../UserConversationControllerTest.php | 32 +++++++++++++++++++ tests/TestCase.php | 10 ++++++ tests/Unit/ExampleTest.php | 18 ----------- 6 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 app/Http/Controllers/UserConversationController.php delete mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Feature/Http/Controllers/UserConversationControllerTest.php delete mode 100644 tests/Unit/ExampleTest.php diff --git a/app/Http/Controllers/UserConversationController.php b/app/Http/Controllers/UserConversationController.php new file mode 100644 index 0000000..ea8a747 --- /dev/null +++ b/app/Http/Controllers/UserConversationController.php @@ -0,0 +1,24 @@ + 'direct']); + + $conversation->users()->attach($user); + + $conversation->users()->attach(auth()->user()); + + $conversation->setRelation('users', new Collection([$user, auth()->user()])); + + return $conversation; + } +} diff --git a/routes/api.php b/routes/api.php index 729db11..a4469b5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,7 @@ use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\DirectMessageController; use App\Http\Controllers\UserController; +use App\Http\Controllers\UserConversationController; use Illuminate\Support\Facades\Route; /* @@ -18,4 +19,7 @@ Route::post('/messages', [DirectMessageController::class, 'new']); Route::get('/messages/{target_user}', [DirectMessageController::class, 'list']); + + Route::post('/conversations/direct/{user}', [UserConversationController::class, 'createDirectConversation']) + ->name('conversations.direct.create'); }); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 1eafba6..0000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,21 +0,0 @@ -get('/'); - - $response->assertStatus(200); - } -} diff --git a/tests/Feature/Http/Controllers/UserConversationControllerTest.php b/tests/Feature/Http/Controllers/UserConversationControllerTest.php new file mode 100644 index 0000000..1fd2fcd --- /dev/null +++ b/tests/Feature/Http/Controllers/UserConversationControllerTest.php @@ -0,0 +1,32 @@ +authenticate(); + + $user = User::factory()->create(); + + $this->post(route('conversations.direct.create', ['user' => $user])) + ->assertCreated(); + + $this->assertDatabaseCount('conversations', 1) + ->assertDatabaseHas('conversations', ['type' => 'direct']); + + $this->assertDatabaseCount('conversation_user', 2) + ->assertDatabaseHas('conversation_user', ['user_id' => auth()->id()]) + ->assertDatabaseHas('conversation_user', ['user_id' => $user->id]); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 2932d4a..0274ea1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,9 +2,19 @@ namespace Tests; +use App\Models\User; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { use CreatesApplication; + + protected function authenticate(): User + { + /** @var \Illuminate\Contracts\Auth\Authenticatable */ + $user = User::factory()->create(); + $this->actingAs($user); + + return $user; + } } diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index e5c5fef..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertTrue(true); - } -} From fcf489a881215f34e23fbb60e9683ab8fe1def4e Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 17 Dec 2022 19:38:18 +0100 Subject: [PATCH 04/22] tableNameFromModel helper --- app/helpers.php | 9 +++++++++ composer.json | 3 +++ .../Http/Controllers/UserConversationControllerTest.php | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 app/helpers.php diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 0000000..929d8b7 --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,9 @@ +post(route('conversations.direct.create', ['user' => $user])) ->assertCreated(); - $this->assertDatabaseCount('conversations', 1) - ->assertDatabaseHas('conversations', ['type' => 'direct']); + $this->assertDatabaseCount(tableNameFromModel(Conversation::class), 1) + ->assertDatabaseHas(tableNameFromModel(Conversation::class), ['type' => 'direct']); $this->assertDatabaseCount('conversation_user', 2) ->assertDatabaseHas('conversation_user', ['user_id' => auth()->id()]) From 0e982b0be0d74c90ba927e3390e9120ec8a9c1d0 Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 17 Dec 2022 19:45:55 +0100 Subject: [PATCH 05/22] conversations.list route --- .../UserConversationController.php | 8 ++++++ ..._221627_create_conversation_user_table.php | 2 ++ routes/api.php | 3 +++ .../UserConversationControllerTest.php | 26 +++++++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/app/Http/Controllers/UserConversationController.php b/app/Http/Controllers/UserConversationController.php index ea8a747..99be26a 100644 --- a/app/Http/Controllers/UserConversationController.php +++ b/app/Http/Controllers/UserConversationController.php @@ -8,6 +8,14 @@ class UserConversationController extends Controller { + public function list(): array + { + /** @var User */ + $current_user = auth()->user(); + + return $current_user->conversations()->latest('id')->get()->toArray(); + } + public function createDirectConversation(User $user) { /** @var Conversation */ diff --git a/database/migrations/2022_12_16_221627_create_conversation_user_table.php b/database/migrations/2022_12_16_221627_create_conversation_user_table.php index e4279c4..050779e 100644 --- a/database/migrations/2022_12_16_221627_create_conversation_user_table.php +++ b/database/migrations/2022_12_16_221627_create_conversation_user_table.php @@ -19,6 +19,8 @@ public function up() $table->foreignId('user_id')->constrained(); $table->foreignId('conversation_id')->constrained(); + + $table->unique(['conversation_id', 'user_id']); }); } diff --git a/routes/api.php b/routes/api.php index a4469b5..5227162 100644 --- a/routes/api.php +++ b/routes/api.php @@ -22,4 +22,7 @@ Route::post('/conversations/direct/{user}', [UserConversationController::class, 'createDirectConversation']) ->name('conversations.direct.create'); + + Route::get('/conversations', [UserConversationController::class, 'list']) + ->name('conversations.list'); }); diff --git a/tests/Feature/Http/Controllers/UserConversationControllerTest.php b/tests/Feature/Http/Controllers/UserConversationControllerTest.php index 62aefe5..921437a 100644 --- a/tests/Feature/Http/Controllers/UserConversationControllerTest.php +++ b/tests/Feature/Http/Controllers/UserConversationControllerTest.php @@ -2,14 +2,40 @@ namespace Tests\Feature\Http\Controllers; +use App\Models\Conversation; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Testing\Fluent\AssertableJson; use Tests\TestCase; class UserConversationControllerTest extends TestCase { use RefreshDatabase; + /** + * @test + */ + public function list() + { + $this->authenticate(); + + Conversation::factory() + ->hasAttached(auth()->user()) + ->hasAttached(User::factory()) + ->count(5) + ->create(); + + $this->get(route('conversations.list')) + ->assertOk() + ->assertJson( + fn (AssertableJson $json) => $json->has(5) + ); + + $this->assertDatabaseCount(tableNameFromModel(Conversation::class), 5); + + $this->assertDatabaseCount('conversation_user', 10); + } + /** * @test */ From 60d5c6bec39e60332800f34348ee0d8b4e0b3d99 Mon Sep 17 00:00:00 2001 From: medilies Date: Fri, 30 Dec 2022 22:59:07 +0100 Subject: [PATCH 06/22] stop seeding direct conversations --- database/seeders/DatabaseSeeder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 86cedd3..5dc79aa 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -26,8 +26,8 @@ public function run() $users_ids = range(1, $other_users->count() + 2); - $this->call(DirectMessagesSeeder::class, false, ['users_ids' => $users_ids]); + // $this->call(DirectMessagesSeeder::class, false, ['users_ids' => $users_ids]); - $this->call(DirectMessagesSeeder::class, false, ['users_ids' => [$first_user->id, $second_user->id], 'count' => 50]); + // $this->call(DirectMessagesSeeder::class, false, ['users_ids' => [$first_user->id, $second_user->id], 'count' => 50]); } } From a26a60f815eab00701e199e6bbb2ba3d13498153 Mon Sep 17 00:00:00 2001 From: medilies Date: Fri, 30 Dec 2022 23:00:00 +0100 Subject: [PATCH 07/22] reorgenizing code --- .../UserConversationController.php | 32 ------------------ .../ConversationController.php | 18 ++++++++++ .../DirectConversationController.php | 15 +++++++++ .../CreateConversationService.php | 24 ++++++++++++++ routes/api.php | 9 ++--- .../DirectConversationControllerTest.php | 33 +++++++++++++++++++ .../UserConversationControllerTest.php | 20 ----------- 7 files changed, 95 insertions(+), 56 deletions(-) delete mode 100644 app/Http/Controllers/UserConversationController.php create mode 100644 app/Http/Controllers/UserConversations/ConversationController.php create mode 100644 app/Http/Controllers/UserConversations/DirectConversationController.php create mode 100644 app/Services/Conversations/CreateConversationService.php create mode 100644 tests/Feature/Http/Controllers/UserConversations/DirectConversationControllerTest.php rename tests/Feature/Http/Controllers/{ => UserConversations}/UserConversationControllerTest.php (56%) diff --git a/app/Http/Controllers/UserConversationController.php b/app/Http/Controllers/UserConversationController.php deleted file mode 100644 index 99be26a..0000000 --- a/app/Http/Controllers/UserConversationController.php +++ /dev/null @@ -1,32 +0,0 @@ -user(); - - return $current_user->conversations()->latest('id')->get()->toArray(); - } - - public function createDirectConversation(User $user) - { - /** @var Conversation */ - $conversation = Conversation::create(['type' => 'direct']); - - $conversation->users()->attach($user); - - $conversation->users()->attach(auth()->user()); - - $conversation->setRelation('users', new Collection([$user, auth()->user()])); - - return $conversation; - } -} diff --git a/app/Http/Controllers/UserConversations/ConversationController.php b/app/Http/Controllers/UserConversations/ConversationController.php new file mode 100644 index 0000000..e6d8d1b --- /dev/null +++ b/app/Http/Controllers/UserConversations/ConversationController.php @@ -0,0 +1,18 @@ +user(); + + // TODO: optimize query and returned JSON + return $current_user->conversations()->latest('id')->get()->load('users')->toArray(); + } +} diff --git a/app/Http/Controllers/UserConversations/DirectConversationController.php b/app/Http/Controllers/UserConversations/DirectConversationController.php new file mode 100644 index 0000000..a1d2237 --- /dev/null +++ b/app/Http/Controllers/UserConversations/DirectConversationController.php @@ -0,0 +1,15 @@ +create(auth()->user(), $user); + } +} diff --git a/app/Services/Conversations/CreateConversationService.php b/app/Services/Conversations/CreateConversationService.php new file mode 100644 index 0000000..bcff1ad --- /dev/null +++ b/app/Services/Conversations/CreateConversationService.php @@ -0,0 +1,24 @@ + 'direct']); + + $conversation->users()->attach($initiator_user); + + $conversation->users()->attach($user); + + $conversation->setRelation('users', new Collection([$user, $initiator_user])); + + return $conversation; + } +} diff --git a/routes/api.php b/routes/api.php index 5227162..cda3586 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,7 +3,8 @@ use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\DirectMessageController; use App\Http\Controllers\UserController; -use App\Http\Controllers\UserConversationController; +use App\Http\Controllers\UserConversations\ConversationController; +use App\Http\Controllers\UserConversations\DirectConversationController; use Illuminate\Support\Facades\Route; /* @@ -20,9 +21,9 @@ Route::post('/messages', [DirectMessageController::class, 'new']); Route::get('/messages/{target_user}', [DirectMessageController::class, 'list']); - Route::post('/conversations/direct/{user}', [UserConversationController::class, 'createDirectConversation']) - ->name('conversations.direct.create'); + Route::post('/conversations/direct/{user}', [DirectConversationController::class, 'start']) + ->name('conversations.direct.start'); - Route::get('/conversations', [UserConversationController::class, 'list']) + Route::get('/conversations', [ConversationController::class, 'list']) ->name('conversations.list'); }); diff --git a/tests/Feature/Http/Controllers/UserConversations/DirectConversationControllerTest.php b/tests/Feature/Http/Controllers/UserConversations/DirectConversationControllerTest.php new file mode 100644 index 0000000..e62ac23 --- /dev/null +++ b/tests/Feature/Http/Controllers/UserConversations/DirectConversationControllerTest.php @@ -0,0 +1,33 @@ +authenticate(); + + $user = User::factory()->create(); + + $this->post(route('conversations.direct.start', ['user' => $user])) + ->assertCreated(); + + $this->assertDatabaseCount(tableNameFromModel(Conversation::class), 1) + ->assertDatabaseHas(tableNameFromModel(Conversation::class), ['type' => 'direct']); + + $this->assertDatabaseCount('conversation_user', 2) + ->assertDatabaseHas('conversation_user', ['user_id' => auth()->id()]) + ->assertDatabaseHas('conversation_user', ['user_id' => $user->id]); + } +} diff --git a/tests/Feature/Http/Controllers/UserConversationControllerTest.php b/tests/Feature/Http/Controllers/UserConversations/UserConversationControllerTest.php similarity index 56% rename from tests/Feature/Http/Controllers/UserConversationControllerTest.php rename to tests/Feature/Http/Controllers/UserConversations/UserConversationControllerTest.php index 921437a..8d873f7 100644 --- a/tests/Feature/Http/Controllers/UserConversationControllerTest.php +++ b/tests/Feature/Http/Controllers/UserConversations/UserConversationControllerTest.php @@ -35,24 +35,4 @@ public function list() $this->assertDatabaseCount('conversation_user', 10); } - - /** - * @test - */ - public function createDirectConversation() - { - $this->authenticate(); - - $user = User::factory()->create(); - - $this->post(route('conversations.direct.create', ['user' => $user])) - ->assertCreated(); - - $this->assertDatabaseCount(tableNameFromModel(Conversation::class), 1) - ->assertDatabaseHas(tableNameFromModel(Conversation::class), ['type' => 'direct']); - - $this->assertDatabaseCount('conversation_user', 2) - ->assertDatabaseHas('conversation_user', ['user_id' => auth()->id()]) - ->assertDatabaseHas('conversation_user', ['user_id' => $user->id]); - } } From 4bea3c185018d2f727fc6bb0242da56841b295bc Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 31 Dec 2022 11:43:09 +0100 Subject: [PATCH 08/22] edit conversations.direct.get route --- .../UserConversations/DirectConversationController.php | 2 +- routes/api.php | 4 ++-- .../UserConversations/DirectConversationControllerTest.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/UserConversations/DirectConversationController.php b/app/Http/Controllers/UserConversations/DirectConversationController.php index a1d2237..93df803 100644 --- a/app/Http/Controllers/UserConversations/DirectConversationController.php +++ b/app/Http/Controllers/UserConversations/DirectConversationController.php @@ -8,7 +8,7 @@ class DirectConversationController extends Controller { - public function start(CreateConversationService $createConversationService, User $user) + public function getConversation(CreateConversationService $createConversationService, User $user) { return $createConversationService->create(auth()->user(), $user); } diff --git a/routes/api.php b/routes/api.php index cda3586..01e3921 100644 --- a/routes/api.php +++ b/routes/api.php @@ -21,8 +21,8 @@ Route::post('/messages', [DirectMessageController::class, 'new']); Route::get('/messages/{target_user}', [DirectMessageController::class, 'list']); - Route::post('/conversations/direct/{user}', [DirectConversationController::class, 'start']) - ->name('conversations.direct.start'); + Route::get('/conversations/direct/{user}', [DirectConversationController::class, 'getConversation']) + ->name('conversations.direct.get'); Route::get('/conversations', [ConversationController::class, 'list']) ->name('conversations.list'); diff --git a/tests/Feature/Http/Controllers/UserConversations/DirectConversationControllerTest.php b/tests/Feature/Http/Controllers/UserConversations/DirectConversationControllerTest.php index e62ac23..f9f9aab 100644 --- a/tests/Feature/Http/Controllers/UserConversations/DirectConversationControllerTest.php +++ b/tests/Feature/Http/Controllers/UserConversations/DirectConversationControllerTest.php @@ -14,13 +14,13 @@ class DirectConversationControllerTest extends TestCase /** * @test */ - public function start() + public function getConversation() { $this->authenticate(); $user = User::factory()->create(); - $this->post(route('conversations.direct.start', ['user' => $user])) + $this->get(route('conversations.direct.get', ['user' => $user])) ->assertCreated(); $this->assertDatabaseCount(tableNameFromModel(Conversation::class), 1) From 11d8cd2ecc30fa0257ef0262fc889633bec26b4e Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 31 Dec 2022 17:56:26 +0100 Subject: [PATCH 09/22] getConversation find or create --- .../DirectConversationController.php | 14 +++++++++-- .../CreateConversationService.php | 23 ++++++++++++++++++- ...2_16_221527_create_conversations_table.php | 2 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/UserConversations/DirectConversationController.php b/app/Http/Controllers/UserConversations/DirectConversationController.php index 93df803..6206cd6 100644 --- a/app/Http/Controllers/UserConversations/DirectConversationController.php +++ b/app/Http/Controllers/UserConversations/DirectConversationController.php @@ -3,13 +3,23 @@ namespace App\Http\Controllers\UserConversations; use App\Http\Controllers\Controller; +use App\Models\Conversation; use App\Models\User; use App\Services\Conversations\CreateConversationService; class DirectConversationController extends Controller { - public function getConversation(CreateConversationService $createConversationService, User $user) + public function getConversation(CreateConversationService $create_conversation_service, User $user) { - return $createConversationService->create(auth()->user(), $user); + $conversation_name = $create_conversation_service->formatDirectConversationName(auth()->user(), $user); + + /** @var Conversation */ + $conversation = Conversation::where('name', $conversation_name)->first(); + + if ($conversation) { + return $conversation->load('users'); + } + + return $create_conversation_service->create(auth()->user(), $user); } } diff --git a/app/Services/Conversations/CreateConversationService.php b/app/Services/Conversations/CreateConversationService.php index bcff1ad..efb89b9 100644 --- a/app/Services/Conversations/CreateConversationService.php +++ b/app/Services/Conversations/CreateConversationService.php @@ -11,7 +11,10 @@ class CreateConversationService public function create(User $initiator_user, User $user): Conversation { /** @var Conversation */ - $conversation = Conversation::create(['type' => 'direct']); + $conversation = Conversation::create([ + 'type' => 'direct', + 'name' => $this->formatDirectConversationName($initiator_user, $user), + ]); $conversation->users()->attach($initiator_user); @@ -21,4 +24,22 @@ public function create(User $initiator_user, User $user): Conversation return $conversation; } + + public function formatDirectConversationName(int|User $user_1, int|User $user_2) + { + return 'D:'.implode( + ',', + $this->getSorted([ + $user_1 instanceof User ? $user_1->id : $user_1, + $user_2 instanceof User ? $user_2->id : $user_2, + ]) + ); + } + + public function getSorted(array $arr): array + { + sort($arr, SORT_NUMERIC); + + return $arr; + } } diff --git a/database/migrations/2022_12_16_221527_create_conversations_table.php b/database/migrations/2022_12_16_221527_create_conversations_table.php index 0e4b2cc..d69d620 100644 --- a/database/migrations/2022_12_16_221527_create_conversations_table.php +++ b/database/migrations/2022_12_16_221527_create_conversations_table.php @@ -19,7 +19,7 @@ public function up() $table->enum('type', ['direct', 'group']); - $table->string('name')->nullable(); + $table->string('name')->unique()->nullable(); $table->string('visibility')->nullable(); }); From 58d966083bf2ce9b9114d84b312d064e4296fdbb Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 31 Dec 2022 18:31:08 +0100 Subject: [PATCH 10/22] ConversationRepository::findDirectConversation --- .../DirectConversationController.php | 14 +++++------ app/Models/Conversation.php | 11 +++++++++ app/Repositories/ConversationRepository.php | 21 +++++++++++++++++ .../CreateConversationService.php | 23 +------------------ ...2_16_221527_create_conversations_table.php | 2 +- 5 files changed, 41 insertions(+), 30 deletions(-) create mode 100644 app/Repositories/ConversationRepository.php diff --git a/app/Http/Controllers/UserConversations/DirectConversationController.php b/app/Http/Controllers/UserConversations/DirectConversationController.php index 6206cd6..e4abbce 100644 --- a/app/Http/Controllers/UserConversations/DirectConversationController.php +++ b/app/Http/Controllers/UserConversations/DirectConversationController.php @@ -3,18 +3,18 @@ namespace App\Http\Controllers\UserConversations; use App\Http\Controllers\Controller; -use App\Models\Conversation; use App\Models\User; +use App\Repositories\ConversationRepository; use App\Services\Conversations\CreateConversationService; class DirectConversationController extends Controller { - public function getConversation(CreateConversationService $create_conversation_service, User $user) - { - $conversation_name = $create_conversation_service->formatDirectConversationName(auth()->user(), $user); - - /** @var Conversation */ - $conversation = Conversation::where('name', $conversation_name)->first(); + public function getConversation( + ConversationRepository $conversation_repository, + CreateConversationService $create_conversation_service, + User $user + ) { + $conversation = $conversation_repository->findDirectConversation(auth()->user(), $user); if ($conversation) { return $conversation->load('users'); diff --git a/app/Models/Conversation.php b/app/Models/Conversation.php index 8a6d09f..b889f6a 100644 --- a/app/Models/Conversation.php +++ b/app/Models/Conversation.php @@ -2,9 +2,11 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Query\Builder; class Conversation extends Model { @@ -25,4 +27,13 @@ public function messages() { return $this->hasMany(Message::class); } + + // -------------------------------------------- + // Scopes + // -------------------------------------------- + + public function scopeDirect(Builder|EloquentBuilder $query): void + { + $query->where('type', 'direct'); + } } diff --git a/app/Repositories/ConversationRepository.php b/app/Repositories/ConversationRepository.php new file mode 100644 index 0000000..393bfb1 --- /dev/null +++ b/app/Repositories/ConversationRepository.php @@ -0,0 +1,21 @@ +select(['conversations.*', 'conversation_user.conversation_id', 'conversation_user.user_id']) + ->whereIn('user_id', [ + $user_1 instanceof User ? $user_1->id : $user_1, + $user_2 instanceof User ? $user_2->id : $user_2, + ]) + ->direct() + ->first(); + } +} diff --git a/app/Services/Conversations/CreateConversationService.php b/app/Services/Conversations/CreateConversationService.php index efb89b9..bcff1ad 100644 --- a/app/Services/Conversations/CreateConversationService.php +++ b/app/Services/Conversations/CreateConversationService.php @@ -11,10 +11,7 @@ class CreateConversationService public function create(User $initiator_user, User $user): Conversation { /** @var Conversation */ - $conversation = Conversation::create([ - 'type' => 'direct', - 'name' => $this->formatDirectConversationName($initiator_user, $user), - ]); + $conversation = Conversation::create(['type' => 'direct']); $conversation->users()->attach($initiator_user); @@ -24,22 +21,4 @@ public function create(User $initiator_user, User $user): Conversation return $conversation; } - - public function formatDirectConversationName(int|User $user_1, int|User $user_2) - { - return 'D:'.implode( - ',', - $this->getSorted([ - $user_1 instanceof User ? $user_1->id : $user_1, - $user_2 instanceof User ? $user_2->id : $user_2, - ]) - ); - } - - public function getSorted(array $arr): array - { - sort($arr, SORT_NUMERIC); - - return $arr; - } } diff --git a/database/migrations/2022_12_16_221527_create_conversations_table.php b/database/migrations/2022_12_16_221527_create_conversations_table.php index d69d620..0e4b2cc 100644 --- a/database/migrations/2022_12_16_221527_create_conversations_table.php +++ b/database/migrations/2022_12_16_221527_create_conversations_table.php @@ -19,7 +19,7 @@ public function up() $table->enum('type', ['direct', 'group']); - $table->string('name')->unique()->nullable(); + $table->string('name')->nullable(); $table->string('visibility')->nullable(); }); From 3c8d221d1d3beb053d54b597d0867658d629f413 Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 31 Dec 2022 19:16:34 +0100 Subject: [PATCH 11/22] Improve/Fix find create DirectConversation --- .../DirectConversationController.php | 10 +----- app/Repositories/ConversationRepository.php | 36 ++++++++++++++++++- .../CreateConversationService.php | 24 ------------- config/database.php | 8 +++++ 4 files changed, 44 insertions(+), 34 deletions(-) delete mode 100644 app/Services/Conversations/CreateConversationService.php diff --git a/app/Http/Controllers/UserConversations/DirectConversationController.php b/app/Http/Controllers/UserConversations/DirectConversationController.php index e4abbce..ecd82e6 100644 --- a/app/Http/Controllers/UserConversations/DirectConversationController.php +++ b/app/Http/Controllers/UserConversations/DirectConversationController.php @@ -5,21 +5,13 @@ use App\Http\Controllers\Controller; use App\Models\User; use App\Repositories\ConversationRepository; -use App\Services\Conversations\CreateConversationService; class DirectConversationController extends Controller { public function getConversation( ConversationRepository $conversation_repository, - CreateConversationService $create_conversation_service, User $user ) { - $conversation = $conversation_repository->findDirectConversation(auth()->user(), $user); - - if ($conversation) { - return $conversation->load('users'); - } - - return $create_conversation_service->create(auth()->user(), $user); + return $conversation_repository->findOrcreateDirectConversation(auth()->user(), $user); } } diff --git a/app/Repositories/ConversationRepository.php b/app/Repositories/ConversationRepository.php index 393bfb1..6ca4bbb 100644 --- a/app/Repositories/ConversationRepository.php +++ b/app/Repositories/ConversationRepository.php @@ -4,18 +4,52 @@ use App\Models\Conversation; use App\Models\User; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Facades\DB; class ConversationRepository { public function findDirectConversation(int|User $user_1, int|User $user_2): ?Conversation { return Conversation::join('conversation_user', 'conversations.id', '=', 'conversation_user.conversation_id') - ->select(['conversations.*', 'conversation_user.conversation_id', 'conversation_user.user_id']) + ->select([ + 'conversations.*', + 'conversation_user.conversation_id as conversation_id', + 'conversation_user.user_id as user_id', + ]) + ->addSelect(DB::raw('COUNT(user_id) as count')) ->whereIn('user_id', [ $user_1 instanceof User ? $user_1->id : $user_1, $user_2 instanceof User ? $user_2->id : $user_2, ]) + ->having('count', 2) ->direct() + ->groupBy('conversation_id') ->first(); } + + public function createDirectConversation(int|User $initiator_user, int|User $user): Conversation + { + /** @var Conversation */ + $conversation = Conversation::create(['type' => 'direct']); + + $conversation->users()->attach($initiator_user); + + $conversation->users()->attach($user); + + $conversation->setRelation('users', new Collection([$user, $initiator_user])); + + return $conversation; + } + + public function findOrcreateDirectConversation(int|User $user_1, int|User $user_2): Conversation + { + $conversation = $this->findDirectConversation($user_1, $user_2); + + if ($conversation) { + return $conversation->load('users'); + } + + return $this->createDirectConversation($user_1, $user_2); + } } diff --git a/app/Services/Conversations/CreateConversationService.php b/app/Services/Conversations/CreateConversationService.php deleted file mode 100644 index bcff1ad..0000000 --- a/app/Services/Conversations/CreateConversationService.php +++ /dev/null @@ -1,24 +0,0 @@ - 'direct']); - - $conversation->users()->attach($initiator_user); - - $conversation->users()->attach($user); - - $conversation->setRelation('users', new Collection([$user, $initiator_user])); - - return $conversation; - } -} diff --git a/config/database.php b/config/database.php index 137ad18..eac902d 100644 --- a/config/database.php +++ b/config/database.php @@ -57,6 +57,14 @@ 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, + 'modes' => [ + //'ONLY_FULL_GROUP_BY', // Disable this to allow grouping by one column + 'STRICT_TRANS_TABLES', + 'NO_ZERO_IN_DATE', + 'NO_ZERO_DATE', + 'ERROR_FOR_DIVISION_BY_ZERO', + 'NO_AUTO_CREATE_USER', + ], 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), From 465973616998d18e336ce294ea2316c52703572f Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 31 Dec 2022 21:07:59 +0100 Subject: [PATCH 12/22] get users on home page --- .../js/modules/chat/pages/MessagesPage.vue | 15 --------------- resources/js/pages/HomePage.vue | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/resources/js/modules/chat/pages/MessagesPage.vue b/resources/js/modules/chat/pages/MessagesPage.vue index 8009520..3dfd2db 100644 --- a/resources/js/modules/chat/pages/MessagesPage.vue +++ b/resources/js/modules/chat/pages/MessagesPage.vue @@ -11,26 +11,11 @@ + From 06542d35db519dd3ee20dd45948622822cf0aefc Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 31 Dec 2022 21:38:04 +0100 Subject: [PATCH 13/22] conversationsStore --- .../js/modules/chat/Components/Inbox.vue | 19 ++++++++++++++++-- resources/js/modules/chat/index.js | 2 ++ .../js/modules/chat/pages/MessagesPage.vue | 4 ---- .../modules/chat/stores/ConversationStore.js | 20 +++++++++++++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 resources/js/modules/chat/stores/ConversationStore.js diff --git a/resources/js/modules/chat/Components/Inbox.vue b/resources/js/modules/chat/Components/Inbox.vue index f7ffd79..15553b4 100644 --- a/resources/js/modules/chat/Components/Inbox.vue +++ b/resources/js/modules/chat/Components/Inbox.vue @@ -1,5 +1,20 @@ - + diff --git a/resources/js/modules/chat/index.js b/resources/js/modules/chat/index.js index 7c17fb1..f45ef86 100644 --- a/resources/js/modules/chat/index.js +++ b/resources/js/modules/chat/index.js @@ -9,11 +9,13 @@ import UsersList from "@/modules/chat/Components/UsersList.vue"; import UsersListItem from "@/modules/chat/Components/UsersListItem.vue"; import { useChatStore } from "@/modules/chat/stores/ChatStore"; +import { useConversationStore } from "@/modules/chat/stores/ConversationStore"; export { MessagesPage, chatRoutes, useChatStore, + useConversationStore, ChatBubble, MessageBox, TheChat, diff --git a/resources/js/modules/chat/pages/MessagesPage.vue b/resources/js/modules/chat/pages/MessagesPage.vue index 3dfd2db..d7921d5 100644 --- a/resources/js/modules/chat/pages/MessagesPage.vue +++ b/resources/js/modules/chat/pages/MessagesPage.vue @@ -1,9 +1,5 @@ diff --git a/resources/js/modules/chat/stores/ConversationStore.js b/resources/js/modules/chat/stores/ConversationStore.js new file mode 100644 index 0000000..c5c086d --- /dev/null +++ b/resources/js/modules/chat/stores/ConversationStore.js @@ -0,0 +1,20 @@ +import { authenticatedGet } from "@/modules/auth"; +import { defineStore } from "pinia"; +import { ref } from "vue"; + +export const useConversationStore = defineStore("conversation", () => { + const conversations = ref({}); + + function refreshConversations() { + authenticatedGet("/api/conversations").then((response) => { + console.log(response.data); + + conversations.value = response.data; + }); + } + + return { + conversations, + refreshConversations, + }; +}); From f2271c93efd3a9f6a36a03d8747278510a6b30df Mon Sep 17 00:00:00 2001 From: medilies Date: Sat, 31 Dec 2022 21:43:33 +0100 Subject: [PATCH 14/22] list conversations --- .../ConversationController.php | 10 ++++--- app/Http/Resources/OtherUsersResource.php | 23 ++++++++++++++++ .../Resources/UserConversationResource.php | 27 +++++++++++++++++++ app/Models/Conversation.php | 5 ++++ .../modules/chat/stores/ConversationStore.js | 2 +- 5 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 app/Http/Resources/OtherUsersResource.php create mode 100644 app/Http/Resources/UserConversationResource.php diff --git a/app/Http/Controllers/UserConversations/ConversationController.php b/app/Http/Controllers/UserConversations/ConversationController.php index e6d8d1b..923ec91 100644 --- a/app/Http/Controllers/UserConversations/ConversationController.php +++ b/app/Http/Controllers/UserConversations/ConversationController.php @@ -3,16 +3,20 @@ namespace App\Http\Controllers\UserConversations; use App\Http\Controllers\Controller; +use App\Http\Resources\UserConversationResource; use App\Models\User; class ConversationController extends Controller { - public function list(): array + public function list() { /** @var User */ $current_user = auth()->user(); - // TODO: optimize query and returned JSON - return $current_user->conversations()->latest('id')->get()->load('users')->toArray(); + return UserConversationResource::collection( + $current_user->conversations()->latest('id') + ->get() + ->load('otherUsers') + ); } } diff --git a/app/Http/Resources/OtherUsersResource.php b/app/Http/Resources/OtherUsersResource.php new file mode 100644 index 0000000..733e9fa --- /dev/null +++ b/app/Http/Resources/OtherUsersResource.php @@ -0,0 +1,23 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + ]; + } +} diff --git a/app/Http/Resources/UserConversationResource.php b/app/Http/Resources/UserConversationResource.php new file mode 100644 index 0000000..102828c --- /dev/null +++ b/app/Http/Resources/UserConversationResource.php @@ -0,0 +1,27 @@ + $this->id, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'type' => $this->type, + 'name' => $this->name, + 'visibility' => $this->visibility, + 'other_users' => OtherUsersResource::collection($this->whenLoaded('otherUsers')), + ] + + ($this->type === 'direct' ? + [] : + ['pivot' => [ + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]]); + } +} diff --git a/app/Models/Conversation.php b/app/Models/Conversation.php index b889f6a..6e2de4e 100644 --- a/app/Models/Conversation.php +++ b/app/Models/Conversation.php @@ -23,6 +23,11 @@ public function users(): BelongsToMany return $this->belongsToMany(User::class)->withTimestamps(); } + public function otherUsers(): BelongsToMany + { + return $this->belongsToMany(User::class)->whereNot('user_id', auth()->id())->withTimestamps(); + } + public function messages() { return $this->hasMany(Message::class); diff --git a/resources/js/modules/chat/stores/ConversationStore.js b/resources/js/modules/chat/stores/ConversationStore.js index c5c086d..b57981b 100644 --- a/resources/js/modules/chat/stores/ConversationStore.js +++ b/resources/js/modules/chat/stores/ConversationStore.js @@ -9,7 +9,7 @@ export const useConversationStore = defineStore("conversation", () => { authenticatedGet("/api/conversations").then((response) => { console.log(response.data); - conversations.value = response.data; + conversations.value = response.data.data; }); } From a3b6e694ca5ae4e3a1d369304b86fcf6f148d80e Mon Sep 17 00:00:00 2001 From: medilies Date: Sun, 1 Jan 2023 23:09:32 +0100 Subject: [PATCH 15/22] getConversationMessages --- .../Controllers/UserConversations/ConversationController.php | 5 +++++ routes/api.php | 2 ++ 2 files changed, 7 insertions(+) diff --git a/app/Http/Controllers/UserConversations/ConversationController.php b/app/Http/Controllers/UserConversations/ConversationController.php index 923ec91..dd02d70 100644 --- a/app/Http/Controllers/UserConversations/ConversationController.php +++ b/app/Http/Controllers/UserConversations/ConversationController.php @@ -19,4 +19,9 @@ public function list() ->load('otherUsers') ); } + + public function getConversationMessages(Conversation $conversation) + { + return $conversation->messages; + } } diff --git a/routes/api.php b/routes/api.php index 01e3921..50670c8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -26,4 +26,6 @@ Route::get('/conversations', [ConversationController::class, 'list']) ->name('conversations.list'); + Route::get('/conversations/{conversation}/messages', [ConversationController::class, 'getConversationMessages']) + ->name('conversations.messages.get'); }); From afb7ef45a33b91a15406f5e6eeef9557047d92fa Mon Sep 17 00:00:00 2001 From: medilies Date: Sun, 1 Jan 2023 23:13:08 +0100 Subject: [PATCH 16/22] newConversationMessage --- .../Controllers/DirectMessageController.php | 88 ------------------- .../ConversationController.php | 8 ++ app/Models/Message.php | 12 --- app/Services/MessageService.php | 20 +++-- ...022_12_16_221727_create_messages_table.php | 1 - database/seeders/DirectMessagesSeeder.php | 1 - routes/api.php | 8 +- 7 files changed, 23 insertions(+), 115 deletions(-) delete mode 100644 app/Http/Controllers/DirectMessageController.php diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php deleted file mode 100644 index 2d4ed85..0000000 --- a/app/Http/Controllers/DirectMessageController.php +++ /dev/null @@ -1,88 +0,0 @@ -store()->getMessageModel()->resource(); - } - - /** - * list - * - * @group DirectMessageController - * - * @urlParam target_user_id int required Example: 2 - * - * @response status=200 scenario=success - * [ - * { - * "id": 1070, - * "created_at": "2022-12-10T11:47:39.000000Z", - * "updated_at": "2022-12-10T11:47:39.000000Z", - * "content": "hooray", - * "target_user_id": 2, - * "user_id": 1, - * "user": { - * "id": 1, - * "name": "medilies", - * "email": "y@y.y", - * "email_verified_at": "2022-12-07T21:47:00.000000Z", - * "created_at": "2022-12-07T21:47:00.000000Z", - * "updated_at": "2022-12-07T21:47:00.000000Z" - * } - * }, - * { - * "id": 1071, - * "created_at": "2022-12-10T11:48:18.000000Z", - * "updated_at": "2022-12-10T11:48:18.000000Z", - * "content": "hip", - * "target_user_id": 1, - * "user_id": 2, - * "user": { - * "id": 2, - * "name": "dummy", - * "email": "e@e.e", - * "email_verified_at": "2022-12-07T21:47:00.000000Z", - * "created_at": "2022-12-07T21:47:00.000000Z", - * "updated_at": "2022-12-07T21:47:00.000000Z" - * } - * } - * ] - */ - public function list(User $target_user): array - { - // TODO: fix returned data according to resource - return DirectMessage::with('user')->whereCorrespondent($target_user->id)->latest('id')->limit(50) - ->get() - ->reverse()->values()->toArray(); - } -} diff --git a/app/Http/Controllers/UserConversations/ConversationController.php b/app/Http/Controllers/UserConversations/ConversationController.php index dd02d70..b9aee67 100644 --- a/app/Http/Controllers/UserConversations/ConversationController.php +++ b/app/Http/Controllers/UserConversations/ConversationController.php @@ -4,7 +4,10 @@ use App\Http\Controllers\Controller; use App\Http\Resources\UserConversationResource; +use App\Models\Conversation; use App\Models\User; +use App\Services\MessageService; +use Illuminate\Http\Request; class ConversationController extends Controller { @@ -24,4 +27,9 @@ public function getConversationMessages(Conversation $conversation) { return $conversation->messages; } + + public function newConversationMessage(Request $request, Conversation $conversation): array + { + return (new MessageService($request, $conversation))->store()->getMessageModel()->resource(); + } } diff --git a/app/Models/Message.php b/app/Models/Message.php index 3d85888..2c1a3f4 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -6,7 +6,6 @@ use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Database\Eloquent\BroadcastableModelEventOccurred; use Illuminate\Database\Eloquent\BroadcastsEvents; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -35,16 +34,6 @@ public function conversation(): BelongsTo // Scopes // -------------------------------------------- - public function scopeWhereCorrespondent(Builder $query, int $target_user_id): void - { - $query->where(function ($q) use ($target_user_id) { - $q->where('user_id', auth()->id())->where('target_user_id', $target_user_id); - }) - ->orWhere(function ($q) use ($target_user_id) { - $q->where('user_id', $target_user_id)->where('target_user_id', auth()->id()); - }); - } - // -------------------------------------------- // Methods // -------------------------------------------- @@ -63,7 +52,6 @@ public function resource(): array 'content' => $this->content, 'created_at' => $this->created_at, 'user_id' => $this->user_id, - 'target_user_id' => $this->target_user_id, 'user' => [ 'id' => $this->user->id, 'name' => $this->user->name, diff --git a/app/Services/MessageService.php b/app/Services/MessageService.php index 7de9e86..3bec70e 100644 --- a/app/Services/MessageService.php +++ b/app/Services/MessageService.php @@ -2,26 +2,29 @@ namespace App\Services; -use App\Models\DirectMessage; +use App\Models\Conversation; +use App\Models\Message; use Exception; use Illuminate\Http\Request; class MessageService { - protected array $message; + protected readonly array $validated_message; - protected ?DirectMessage $messageModel = null; + protected ?Message $messageModel = null; - public function __construct(Request|array $data) - { + public function __construct( + Request|array $data, + protected Conversation $conversation + ) { if ($data instanceof Request) { $data = $data->all(); } - $this->message = $this->validate($data); + $this->validated_message = $this->validate($data); } - public function getMessageModel(): DirectMessage + public function getMessageModel(): Message { return $this->messageModel; } @@ -37,7 +40,7 @@ public function store(): static throw new Exception('The message entity is already stored'); } - $this->messageModel = DirectMessage::create($this->message + ['user_id' => auth()->id()]); + $this->messageModel = $this->conversation->messages()->create($this->validated_message + ['user_id' => auth()->id()]); $this->messageModel->setRelation('user', auth()->user()); @@ -48,7 +51,6 @@ protected function getRules(): array { return [ 'content' => ['required', 'string'], - 'target_user_id' => ['integer'], ]; } } diff --git a/database/migrations/2022_12_16_221727_create_messages_table.php b/database/migrations/2022_12_16_221727_create_messages_table.php index ce64000..dd196f1 100644 --- a/database/migrations/2022_12_16_221727_create_messages_table.php +++ b/database/migrations/2022_12_16_221727_create_messages_table.php @@ -16,7 +16,6 @@ public function up(): void $table->foreignId('user_id')->constrained(); - $table->unsignedBigInteger('target_user_id'); $table->foreignId('conversation_id')->constrained(); }); } diff --git a/database/seeders/DirectMessagesSeeder.php b/database/seeders/DirectMessagesSeeder.php index 5dd8043..aa1fd0a 100644 --- a/database/seeders/DirectMessagesSeeder.php +++ b/database/seeders/DirectMessagesSeeder.php @@ -20,7 +20,6 @@ public function run($users_ids, $count = 1000) for ($i = 0; $i < $count; $i++) { $users = [ 'user_id' => $faker->randomElement($users_ids), - 'target_user_id' => $faker->randomElement($users_ids), ]; $direct_messages[] = DirectMessage::factory($users)->make()->toArray(); diff --git a/routes/api.php b/routes/api.php index 50670c8..b59beee 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,7 +1,6 @@ group(function () { Route::get('/users', [UserController::class, 'index']); - Route::post('/messages', [DirectMessageController::class, 'new']); - Route::get('/messages/{target_user}', [DirectMessageController::class, 'list']); - Route::get('/conversations/direct/{user}', [DirectConversationController::class, 'getConversation']) ->name('conversations.direct.get'); Route::get('/conversations', [ConversationController::class, 'list']) ->name('conversations.list'); + + Route::post('/conversations/{conversation}/messages', [ConversationController::class, 'newConversationMessage']) + ->name('conversations.messages.new'); + Route::get('/conversations/{conversation}/messages', [ConversationController::class, 'getConversationMessages']) ->name('conversations.messages.get'); }); From bbb88ef28b7e6a59304c42cba92d1aef17646a3d Mon Sep 17 00:00:00 2001 From: medilies Date: Tue, 3 Jan 2023 20:46:14 +0100 Subject: [PATCH 17/22] horrible commit --- app/Events/MessageEvent.php | 37 +++++++++++++ .../ConversationController.php | 6 ++- app/Models/Message.php | 52 +------------------ app/Services/MessageService.php | 21 ++++++-- ...tMessageFactory.php => MessageFactory.php} | 2 +- resources/js/App.vue | 25 ++++++++- .../auth/Services/AuthenticatedRequest.js | 7 ++- .../Components/ConversationDirectListItem.vue | 20 +++++++ .../js/modules/chat/Components/Inbox.vue | 4 +- .../js/modules/chat/Components/MessageBox.vue | 13 ++--- .../js/modules/chat/Components/TheChat.vue | 16 +++--- .../modules/chat/Components/UsersListItem.vue | 2 +- .../js/modules/chat/pages/MessagesPage.vue | 30 +---------- resources/js/modules/chat/routes/index.js | 2 +- resources/js/modules/chat/stores/ChatStore.js | 20 ++----- 15 files changed, 134 insertions(+), 123 deletions(-) create mode 100644 app/Events/MessageEvent.php rename database/factories/{DirectMessageFactory.php => MessageFactory.php} (90%) create mode 100644 resources/js/modules/chat/Components/ConversationDirectListItem.vue diff --git a/app/Events/MessageEvent.php b/app/Events/MessageEvent.php new file mode 100644 index 0000000..9e534ec --- /dev/null +++ b/app/Events/MessageEvent.php @@ -0,0 +1,37 @@ +message->conversation->otherUsers->first()->id}"); + } + + /** Get the data to broadcast. */ + public function broadcastWith(): array + { + return $this->message->toArray(); + } +} diff --git a/app/Http/Controllers/UserConversations/ConversationController.php b/app/Http/Controllers/UserConversations/ConversationController.php index b9aee67..0a3f1ba 100644 --- a/app/Http/Controllers/UserConversations/ConversationController.php +++ b/app/Http/Controllers/UserConversations/ConversationController.php @@ -25,11 +25,13 @@ public function list() public function getConversationMessages(Conversation $conversation) { - return $conversation->messages; + return $conversation->messages->load('user'); } public function newConversationMessage(Request $request, Conversation $conversation): array { - return (new MessageService($request, $conversation))->store()->getMessageModel()->resource(); + $conversation->load('users'); + + return (new MessageService($request, $conversation))->store()->broadcast()->getMessageModel()->toArray(); } } diff --git a/app/Models/Message.php b/app/Models/Message.php index 2c1a3f4..969860c 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -3,16 +3,13 @@ namespace App\Models; use Exception; -use Illuminate\Broadcasting\PrivateChannel; -use Illuminate\Database\Eloquent\BroadcastableModelEventOccurred; -use Illuminate\Database\Eloquent\BroadcastsEvents; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Message extends Model { - use BroadcastsEvents, HasFactory; + use HasFactory; protected $guarded = ['id']; @@ -43,56 +40,11 @@ public function conversation(): BelongsTo */ public function resource(): array { - if (! $this->relationLoaded('user')) { - throw new Exception('Load user relation before calling this'); - } - return [ 'id' => $this->id, 'content' => $this->content, 'created_at' => $this->created_at, - 'user_id' => $this->user_id, - 'user' => [ - 'id' => $this->user->id, - 'name' => $this->user->name, - ], + 'conversation_id' => $this->conversation_id, ]; } - - // -------------------------------------------- - // Broadcasting - // -------------------------------------------- - - public function broadcastOn($event) - { - return match ($event) { - 'created' => [new PrivateChannel("direct-messages.{$this->target_user_id}")], - default => [], - }; - } - - public function broadcastAs($event) - { - return match ($event) { - 'created' => 'DirectMessage', - default => null, - }; - } - - public function broadcastWith($event) - { - $this->load('user'); - - return match ($event) { - default => $this->resource(), - }; - } - - protected function newBroadcastableEvent($event) - { - return (new BroadcastableModelEventOccurred( - $this, - $event - ))->dontBroadcastToCurrentUser(); - } } diff --git a/app/Services/MessageService.php b/app/Services/MessageService.php index 3bec70e..6801551 100644 --- a/app/Services/MessageService.php +++ b/app/Services/MessageService.php @@ -2,14 +2,16 @@ namespace App\Services; +use App\Events\MessageEvent; use App\Models\Conversation; use App\Models\Message; use Exception; use Illuminate\Http\Request; +use Illuminate\Support\Collection; class MessageService { - protected readonly array $validated_message; + protected Collection $validated_message; protected ?Message $messageModel = null; @@ -21,7 +23,7 @@ public function __construct( $data = $data->all(); } - $this->validated_message = $this->validate($data); + $this->validated_message = collect($this->validate($data)); } public function getMessageModel(): Message @@ -40,9 +42,20 @@ public function store(): static throw new Exception('The message entity is already stored'); } - $this->messageModel = $this->conversation->messages()->create($this->validated_message + ['user_id' => auth()->id()]); + $this->messageModel = $this->conversation->messages()->create( + $this->validated_message->only('content')->toArray() + + ['user_id' => auth()->id()] + ); - $this->messageModel->setRelation('user', auth()->user()); + $this->messageModel->load('user'); + $this->messageModel->load('conversation.otherUsers'); + + return $this; + } + + public function broadcast(): static + { + MessageEvent::broadcast($this->messageModel); return $this; } diff --git a/database/factories/DirectMessageFactory.php b/database/factories/MessageFactory.php similarity index 90% rename from database/factories/DirectMessageFactory.php rename to database/factories/MessageFactory.php index 27b0001..049f998 100644 --- a/database/factories/DirectMessageFactory.php +++ b/database/factories/MessageFactory.php @@ -7,7 +7,7 @@ /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\DirectMessage> */ -class DirectMessageFactory extends Factory +class MessageFactory extends Factory { /** * Define the model's default state. diff --git a/resources/js/App.vue b/resources/js/App.vue index 113e252..39b2801 100644 --- a/resources/js/App.vue +++ b/resources/js/App.vue @@ -26,9 +26,30 @@ diff --git a/resources/js/modules/auth/Services/AuthenticatedRequest.js b/resources/js/modules/auth/Services/AuthenticatedRequest.js index 71f657c..d935cb3 100644 --- a/resources/js/modules/auth/Services/AuthenticatedRequest.js +++ b/resources/js/modules/auth/Services/AuthenticatedRequest.js @@ -19,8 +19,11 @@ const authenticatedGet = (path, additionalHeaders = {}) => { }); }; -const sendMessage = (message) => { - return authenticatedPost("/api/messages", message); +const sendMessage = (message, conversationId) => { + return authenticatedPost( + `/api/conversations/${conversationId}/messages`, + message + ); }; export { authenticatedPost, authenticatedGet, sendMessage }; diff --git a/resources/js/modules/chat/Components/ConversationDirectListItem.vue b/resources/js/modules/chat/Components/ConversationDirectListItem.vue new file mode 100644 index 0000000..6d726f9 --- /dev/null +++ b/resources/js/modules/chat/Components/ConversationDirectListItem.vue @@ -0,0 +1,20 @@ + + + diff --git a/resources/js/modules/chat/Components/Inbox.vue b/resources/js/modules/chat/Components/Inbox.vue index 15553b4..999a71e 100644 --- a/resources/js/modules/chat/Components/Inbox.vue +++ b/resources/js/modules/chat/Components/Inbox.vue @@ -4,13 +4,13 @@ v-for="conversation in conversationsStore.conversations" key="conversation.id" > - {{ conversation.id }} + diff --git a/resources/js/modules/chat/Components/UsersListItem.vue b/resources/js/modules/chat/Components/UsersListItem.vue index d74244a..7fcd882 100644 --- a/resources/js/modules/chat/Components/UsersListItem.vue +++ b/resources/js/modules/chat/Components/UsersListItem.vue @@ -2,7 +2,7 @@ diff --git a/resources/js/modules/chat/pages/MessagesPage.vue b/resources/js/modules/chat/pages/MessagesPage.vue index d7921d5..a1195b0 100644 --- a/resources/js/modules/chat/pages/MessagesPage.vue +++ b/resources/js/modules/chat/pages/MessagesPage.vue @@ -4,32 +4,4 @@ - + diff --git a/resources/js/modules/chat/routes/index.js b/resources/js/modules/chat/routes/index.js index 60a1766..4e938b2 100644 --- a/resources/js/modules/chat/routes/index.js +++ b/resources/js/modules/chat/routes/index.js @@ -19,7 +19,7 @@ export default [ beforeEnter: [authGuard], }, { - path: "direct/:direct_messages_target_user_id", + path: "direct/:conversation_id", name: "messages.direct", component: TheChat, beforeEnter: [authGuard], diff --git a/resources/js/modules/chat/stores/ChatStore.js b/resources/js/modules/chat/stores/ChatStore.js index f41ab1d..0d6f20d 100644 --- a/resources/js/modules/chat/stores/ChatStore.js +++ b/resources/js/modules/chat/stores/ChatStore.js @@ -4,34 +4,22 @@ import { useAuthStore } from "@/modules/auth/store/AuthStore"; import { useRoute } from "vue-router"; export const useChatStore = defineStore("chat", () => { - const authStore = useAuthStore(); const route = useRoute(); const messages = ref({}); const getCurrentChatMessages = computed(() => { - if ( - !messages.value[ - parseInt(route.params.direct_messages_target_user_id) - ] - ) { - messages.value[ - parseInt(route.params.direct_messages_target_user_id) - ] = []; + if (!messages.value[parseInt(route.params.conversation_id)]) { + messages.value[parseInt(route.params.conversation_id)] = []; } - return messages.value[ - parseInt(route.params.direct_messages_target_user_id) - ]; + return messages.value[parseInt(route.params.conversation_id)]; }); function storeNewMessage(message) { // console.log(message); - let chatId = - authStore.user.id === message.user_id - ? message.target_user_id - : message.user_id; + let chatId = message.conversation_id; if (!messages.value[chatId]) { messages.value[chatId] = []; From b789b7fa55a6501b54f0b2d8d62ba319e5a5ac7b Mon Sep 17 00:00:00 2001 From: medilies Date: Tue, 3 Jan 2023 21:31:23 +0100 Subject: [PATCH 18/22] cleanup --- app/Events/MessageEvent.php | 4 +-- .../DirectConversationController.php | 2 +- app/Repositories/ConversationRepository.php | 2 +- database/factories/MessageFactory.php | 2 +- database/seeders/DatabaseSeeder.php | 4 --- database/seeders/DirectMessagesSeeder.php | 30 ------------------- resources/js/App.vue | 12 ++++---- .../Components/ConversationDirectListItem.vue | 2 +- .../{TheChat.vue => TheConversation.vue} | 2 +- .../modules/chat/Components/UsersListItem.vue | 4 +-- resources/js/modules/chat/index.js | 4 +-- resources/js/modules/chat/routes/index.js | 14 ++++----- resources/js/modules/chat/stores/ChatStore.js | 2 ++ routes/channels.php | 4 +-- routes/web.php | 8 ++--- 15 files changed, 28 insertions(+), 68 deletions(-) delete mode 100644 database/seeders/DirectMessagesSeeder.php rename resources/js/modules/chat/Components/{TheChat.vue => TheConversation.vue} (97%) diff --git a/app/Events/MessageEvent.php b/app/Events/MessageEvent.php index 9e534ec..0c58abe 100644 --- a/app/Events/MessageEvent.php +++ b/app/Events/MessageEvent.php @@ -16,7 +16,6 @@ class MessageEvent implements ShouldBroadcast public function __construct( protected Message $message ) { - // } /** @@ -26,10 +25,9 @@ public function __construct( */ public function broadcastOn() { - return new PrivateChannel("direct-messages.{$this->message->conversation->otherUsers->first()->id}"); + return new PrivateChannel("chat.{$this->message->conversation->otherUsers->first()->id}"); } - /** Get the data to broadcast. */ public function broadcastWith(): array { return $this->message->toArray(); diff --git a/app/Http/Controllers/UserConversations/DirectConversationController.php b/app/Http/Controllers/UserConversations/DirectConversationController.php index ecd82e6..924778b 100644 --- a/app/Http/Controllers/UserConversations/DirectConversationController.php +++ b/app/Http/Controllers/UserConversations/DirectConversationController.php @@ -12,6 +12,6 @@ public function getConversation( ConversationRepository $conversation_repository, User $user ) { - return $conversation_repository->findOrcreateDirectConversation(auth()->user(), $user); + return $conversation_repository->findOrCreateDirectConversation(auth()->user(), $user); } } diff --git a/app/Repositories/ConversationRepository.php b/app/Repositories/ConversationRepository.php index 6ca4bbb..80ab914 100644 --- a/app/Repositories/ConversationRepository.php +++ b/app/Repositories/ConversationRepository.php @@ -42,7 +42,7 @@ public function createDirectConversation(int|User $initiator_user, int|User $use return $conversation; } - public function findOrcreateDirectConversation(int|User $user_1, int|User $user_2): Conversation + public function findOrCreateDirectConversation(int|User $user_1, int|User $user_2): Conversation { $conversation = $this->findDirectConversation($user_1, $user_2); diff --git a/database/factories/MessageFactory.php b/database/factories/MessageFactory.php index 049f998..7f72c2e 100644 --- a/database/factories/MessageFactory.php +++ b/database/factories/MessageFactory.php @@ -5,7 +5,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\DirectMessage> + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Message> */ class MessageFactory extends Factory { diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 5dc79aa..6c0720e 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -25,9 +25,5 @@ public function run() $other_users = User::factory(30)->create(); $users_ids = range(1, $other_users->count() + 2); - - // $this->call(DirectMessagesSeeder::class, false, ['users_ids' => $users_ids]); - - // $this->call(DirectMessagesSeeder::class, false, ['users_ids' => [$first_user->id, $second_user->id], 'count' => 50]); } } diff --git a/database/seeders/DirectMessagesSeeder.php b/database/seeders/DirectMessagesSeeder.php deleted file mode 100644 index aa1fd0a..0000000 --- a/database/seeders/DirectMessagesSeeder.php +++ /dev/null @@ -1,30 +0,0 @@ - $faker->randomElement($users_ids), - ]; - - $direct_messages[] = DirectMessage::factory($users)->make()->toArray(); - } - - DirectMessage::insert($direct_messages); - } -} diff --git a/resources/js/App.vue b/resources/js/App.vue index 39b2801..6a5122a 100644 --- a/resources/js/App.vue +++ b/resources/js/App.vue @@ -6,9 +6,7 @@
Home - - Inbox - + Inbox
@@ -39,7 +37,7 @@ const chatStore = useChatStore(); const authStore = useAuthStore(); if (authStore.user) { - Echo.private(`direct-messages.${authStore.user.id}`).listen( + Echo.private(`chat.${authStore.user.id}`).listen( "MessageEvent", (message) => { // console.log(message); @@ -48,8 +46,8 @@ if (authStore.user) { } ); - Echo.private("chat").listenForWhisper("typing", (e) => { - console.log("typing..."); - }); + // Echo.private("chat").listenForWhisper("typing", (e) => { + // console.log("typing..."); + // }); } diff --git a/resources/js/modules/chat/Components/ConversationDirectListItem.vue b/resources/js/modules/chat/Components/ConversationDirectListItem.vue index 6d726f9..ef91e6b 100644 --- a/resources/js/modules/chat/Components/ConversationDirectListItem.vue +++ b/resources/js/modules/chat/Components/ConversationDirectListItem.vue @@ -1,7 +1,7 @@