feat: initial release of relaticle/comments
Filament comments package with: - Polymorphic commenting on any Eloquent model - Threaded replies with configurable depth - @mentions with autocomplete and user search - Emoji reactions with toggle and who-reacted tooltips - File attachments via Livewire uploads - Reply and mention notifications via Filament notification system - Thread subscriptions for notification control - Real-time broadcasting (opt-in Echo) with polling fallback - Dark mode support - CommentsAction, CommentsTableAction, CommentsEntry for Filament integration - 204 tests, 421 assertions
This commit is contained in:
183
src/Livewire/CommentItem.php
Normal file
183
src/Livewire/CommentItem.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace Relaticle\Comments\Livewire;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Livewire\WithFileUploads;
|
||||
use Relaticle\Comments\Comment;
|
||||
use Relaticle\Comments\Config;
|
||||
use Relaticle\Comments\Contracts\MentionResolver;
|
||||
use Relaticle\Comments\Events\CommentCreated;
|
||||
use Relaticle\Comments\Events\CommentDeleted;
|
||||
use Relaticle\Comments\Events\CommentUpdated;
|
||||
use Relaticle\Comments\Mentions\MentionParser;
|
||||
|
||||
class CommentItem extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public Comment $comment;
|
||||
|
||||
public bool $isEditing = false;
|
||||
|
||||
public bool $isReplying = false;
|
||||
|
||||
public string $editBody = '';
|
||||
|
||||
public string $replyBody = '';
|
||||
|
||||
/** @var array<int, TemporaryUploadedFile> */
|
||||
public array $replyAttachments = [];
|
||||
|
||||
public function mount(Comment $comment): void
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
public function startEdit(): void
|
||||
{
|
||||
$this->authorize('update', $this->comment);
|
||||
|
||||
$this->isEditing = true;
|
||||
$this->editBody = $this->comment->body;
|
||||
}
|
||||
|
||||
public function cancelEdit(): void
|
||||
{
|
||||
$this->isEditing = false;
|
||||
$this->editBody = '';
|
||||
}
|
||||
|
||||
public function saveEdit(): void
|
||||
{
|
||||
$this->authorize('update', $this->comment);
|
||||
|
||||
$this->validate([
|
||||
'editBody' => ['required', 'string', 'min:1'],
|
||||
]);
|
||||
|
||||
$this->comment->update([
|
||||
'body' => $this->editBody,
|
||||
'edited_at' => now(),
|
||||
]);
|
||||
|
||||
event(new CommentUpdated($this->comment->fresh()));
|
||||
|
||||
app(MentionParser::class)->syncMentions($this->comment->fresh());
|
||||
|
||||
$this->dispatch('commentUpdated');
|
||||
|
||||
$this->isEditing = false;
|
||||
$this->editBody = '';
|
||||
}
|
||||
|
||||
public function deleteComment(): void
|
||||
{
|
||||
$this->authorize('delete', $this->comment);
|
||||
|
||||
$this->comment->delete();
|
||||
|
||||
event(new CommentDeleted($this->comment));
|
||||
|
||||
$this->dispatch('commentDeleted');
|
||||
}
|
||||
|
||||
public function startReply(): void
|
||||
{
|
||||
if (! $this->comment->canReply()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isReplying = true;
|
||||
}
|
||||
|
||||
public function cancelReply(): void
|
||||
{
|
||||
$this->isReplying = false;
|
||||
$this->replyBody = '';
|
||||
$this->replyAttachments = [];
|
||||
}
|
||||
|
||||
public function addReply(): void
|
||||
{
|
||||
$this->authorize('reply', $this->comment);
|
||||
|
||||
$rules = ['replyBody' => ['required', 'string', 'min:1']];
|
||||
|
||||
if (Config::areAttachmentsEnabled()) {
|
||||
$maxSize = Config::getAttachmentMaxSize();
|
||||
$allowedTypes = implode(',', Config::getAttachmentAllowedTypes());
|
||||
$rules['replyAttachments.*'] = ['nullable', 'file', "max:{$maxSize}", "mimetypes:{$allowedTypes}"];
|
||||
}
|
||||
|
||||
$this->validate($rules);
|
||||
|
||||
$user = Config::resolveAuthenticatedUser();
|
||||
|
||||
$reply = $this->comment->commentable->comments()->create([
|
||||
'body' => $this->replyBody,
|
||||
'parent_id' => $this->comment->id,
|
||||
'user_id' => $user->getKey(),
|
||||
'user_type' => $user->getMorphClass(),
|
||||
]);
|
||||
|
||||
if (Config::areAttachmentsEnabled() && ! empty($this->replyAttachments)) {
|
||||
$disk = Config::getAttachmentDisk();
|
||||
|
||||
foreach ($this->replyAttachments as $file) {
|
||||
$path = $file->store("comments/attachments/{$reply->id}", $disk);
|
||||
|
||||
$reply->attachments()->create([
|
||||
'file_path' => $path,
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'size' => $file->getSize(),
|
||||
'disk' => $disk,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
event(new CommentCreated($reply));
|
||||
|
||||
app(MentionParser::class)->syncMentions($reply);
|
||||
|
||||
$this->dispatch('commentUpdated');
|
||||
|
||||
$this->isReplying = false;
|
||||
$this->replyBody = '';
|
||||
$this->replyAttachments = [];
|
||||
}
|
||||
|
||||
public function removeReplyAttachment(int $index): void
|
||||
{
|
||||
$attachments = $this->replyAttachments;
|
||||
unset($attachments[$index]);
|
||||
$this->replyAttachments = array_values($attachments);
|
||||
}
|
||||
|
||||
/** @return array<int, array{id: int, name: string, avatar_url: ?string}> */
|
||||
public function searchUsers(string $query): array
|
||||
{
|
||||
if (mb_strlen($query) < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$resolver = app(MentionResolver::class);
|
||||
|
||||
return $resolver->search($query)
|
||||
->map(fn ($user) => [
|
||||
'id' => $user->getKey(),
|
||||
'name' => $user->getCommentName(),
|
||||
'avatar_url' => $user->getCommentAvatarUrl(),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('comments::livewire.comment-item');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user