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();
+});