diff --git a/app-modules/panel-app/resources/views/livewire/timeline/reactions.blade.php b/app-modules/panel-app/resources/views/livewire/timeline/reactions.blade.php new file mode 100644 index 000000000..af5cf1aeb --- /dev/null +++ b/app-modules/panel-app/resources/views/livewire/timeline/reactions.blade.php @@ -0,0 +1,40 @@ +@php + use He4rt\Activity\Reaction\Enums\TimelineReaction; +@endphp + +
+ + +
+ @foreach ($reactions as $reaction) + + @endforeach +
+
diff --git a/app-modules/panel-app/src/Livewire/Timeline/Reactions.php b/app-modules/panel-app/src/Livewire/Timeline/Reactions.php new file mode 100644 index 000000000..7cb808063 --- /dev/null +++ b/app-modules/panel-app/src/Livewire/Timeline/Reactions.php @@ -0,0 +1,64 @@ + */ + public array $counts = []; + + public ?string $mine = null; + + /** @param array $counts */ + public function mount(string $timelineId, array $counts = [], ?string $mine = null): void + { + $this->timelineId = $timelineId; + $this->counts = $counts; + $this->mine = $mine; + } + + public function reactWith(string $reaction, #[CurrentUser] ?User $user): void + { + if (!$user instanceof User) { + return; + } + + $case = TimelineReaction::tryFrom($reaction); + if ($case === null) { + return; + } + + resolve(ReactWith::class)->handle(new ReactWithDTO($user->id, $this->timelineId, $case)); + + $summary = resolve(ReactionSummary::class) + ->forTimelines([$this->timelineId], $user->id) + ->first(); + + $this->counts = $summary->counts; + $this->mine = $summary->mine?->value; + + $this->dispatch('timeline.reaction-updated'); + } + + public function render(): View + { + return view('panel-app::livewire.timeline.reactions', [ + 'reactions' => TimelineReaction::cases(), + ]); + } +} diff --git a/app-modules/panel-app/src/PanelAppServiceProvider.php b/app-modules/panel-app/src/PanelAppServiceProvider.php index 787975310..fbd83ce6d 100644 --- a/app-modules/panel-app/src/PanelAppServiceProvider.php +++ b/app-modules/panel-app/src/PanelAppServiceProvider.php @@ -13,6 +13,7 @@ use He4rt\PanelApp\Livewire\Timeline\Composer; use He4rt\PanelApp\Livewire\Timeline\Feed; use He4rt\PanelApp\Livewire\Timeline\PostShow; +use He4rt\PanelApp\Livewire\Timeline\Reactions; use He4rt\PanelApp\Livewire\Timeline\ReplyComposer; use He4rt\PanelApp\Livewire\Timeline\ThreadReplies; use Illuminate\Support\ServiceProvider; @@ -41,5 +42,6 @@ public function boot(): void Livewire::component('timeline-post-show', PostShow::class); Livewire::component('timeline-reply-composer', ReplyComposer::class); Livewire::component('timeline-thread-replies', ThreadReplies::class); + Livewire::component('timeline-reactions', Reactions::class); } } diff --git a/app-modules/panel-app/tests/Feature/Timeline/ReactionsTest.php b/app-modules/panel-app/tests/Feature/Timeline/ReactionsTest.php new file mode 100644 index 000000000..9a15d0b48 --- /dev/null +++ b/app-modules/panel-app/tests/Feature/Timeline/ReactionsTest.php @@ -0,0 +1,122 @@ +user = User::factory()->create(); + + $postEntry = PostEntry::factory()->create(); + $this->post = Timeline::factory()->create([ + 'postable_type' => (new PostEntry)->getMorphClass(), + 'postable_id' => $postEntry->id, + ]); +}); + +test('shows the six available reactions with icon and label', function (): void { + $this->actingAs($this->user); + + livewire(Reactions::class, ['timelineId' => $this->post->id]) + ->assertSee('Curtir') + ->assertSee('Amei') + ->assertSee('Haha') + ->assertSee('Parabéns') + ->assertSee('Fogo') + ->assertSee('Triste'); +}); + +test('registers a reaction, updates state and dispatches the update event', function (): void { + $this->actingAs($this->user); + + livewire(Reactions::class, ['timelineId' => $this->post->id]) + ->call('reactWith', TimelineReaction::Love->value) + ->assertDispatched('timeline.reaction-updated') + ->assertSet('mine', TimelineReaction::Love->value) + ->assertSet('counts', [TimelineReaction::Love->value => 1]); + + expect(UserReaction::query() + ->where('user_id', $this->user->id) + ->where('timeline_id', $this->post->id) + ->first()?->reaction)->toBe(TimelineReaction::Love); +}); + +test('replaces an existing reaction with another one', function (): void { + $this->actingAs($this->user); + + UserReaction::factory()->create([ + 'user_id' => $this->user->id, + 'timeline_id' => $this->post->id, + 'reaction' => TimelineReaction::Like, + ]); + + livewire(Reactions::class, [ + 'timelineId' => $this->post->id, + 'counts' => [TimelineReaction::Like->value => 1], + 'mine' => TimelineReaction::Like->value, + ]) + ->call('reactWith', TimelineReaction::Laugh->value) + ->assertDispatched('timeline.reaction-updated') + ->assertSet('mine', TimelineReaction::Laugh->value) + ->assertSet('counts', [TimelineReaction::Laugh->value => 1]); + + expect(UserReaction::query() + ->where('user_id', $this->user->id) + ->where('timeline_id', $this->post->id) + ->count())->toBe(1) + ->and(UserReaction::query()->first()?->reaction)->toBe(TimelineReaction::Laugh); +}); + +test('toggles off the current reaction when chosen again', function (): void { + $this->actingAs($this->user); + + UserReaction::factory()->create([ + 'user_id' => $this->user->id, + 'timeline_id' => $this->post->id, + 'reaction' => TimelineReaction::Like, + ]); + + livewire(Reactions::class, [ + 'timelineId' => $this->post->id, + 'counts' => [TimelineReaction::Like->value => 1], + 'mine' => TimelineReaction::Like->value, + ]) + ->call('reactWith', TimelineReaction::Like->value) + ->assertDispatched('timeline.reaction-updated') + ->assertSet('mine', null) + ->assertSet('counts', []); + + expect(UserReaction::query() + ->where('user_id', $this->user->id) + ->where('timeline_id', $this->post->id) + ->exists())->toBeFalse(); +}); + +test('rejects a reaction outside the closed set', function (): void { + $this->actingAs($this->user); + + livewire(Reactions::class, ['timelineId' => $this->post->id]) + ->call('reactWith', 'xpto') + ->assertNotDispatched('timeline.reaction-updated'); + + expect(UserReaction::query() + ->where('timeline_id', $this->post->id) + ->exists())->toBeFalse(); +}); + +test('blocks an unauthenticated user from reacting', function (): void { + livewire(Reactions::class, ['timelineId' => $this->post->id]) + ->call('reactWith', TimelineReaction::Love->value) + ->assertNotDispatched('timeline.reaction-updated'); + + expect(UserReaction::query() + ->where('timeline_id', $this->post->id) + ->exists())->toBeFalse(); +});