diff --git a/src/CommentsConfig.php b/src/CommentsConfig.php
index 4c64446..a6f2c33 100644
--- a/src/CommentsConfig.php
+++ b/src/CommentsConfig.php
@@ -4,6 +4,7 @@ namespace Relaticle\Comments;
use App\Models\User;
use Closure;
+use Filament\Forms\Components\RichEditor\MentionProvider;
use Relaticle\Comments\Mentions\DefaultMentionResolver;
use Relaticle\Comments\Models\Comment;
use Relaticle\Comments\Policies\CommentPolicy;
@@ -173,4 +174,19 @@ class CommentsConfig
{
static::$resolveAuthenticatedUser = $callback;
}
+
+ public static function makeMentionProvider(): MentionProvider
+ {
+ return MentionProvider::make('@')
+ ->getSearchResultsUsing(fn (string $search): array => static::getCommenterModel()::query()
+ ->where('name', 'like', "%{$search}%")
+ ->orderBy('name')
+ ->limit(static::getMentionMaxResults())
+ ->pluck('name', 'id')
+ ->all())
+ ->getLabelsUsing(fn (array $ids): array => static::getCommenterModel()::query()
+ ->whereIn('id', $ids)
+ ->pluck('name', 'id')
+ ->all());
+ }
}
diff --git a/src/Livewire/CommentItem.php b/src/Livewire/CommentItem.php
index b862e86..ead7671 100644
--- a/src/Livewire/CommentItem.php
+++ b/src/Livewire/CommentItem.php
@@ -2,20 +2,24 @@
namespace Relaticle\Comments\Livewire;
+use Filament\Forms\Components\RichEditor;
+use Filament\Forms\Concerns\InteractsWithForms;
+use Filament\Forms\Contracts\HasForms;
+use Filament\Schemas\Schema;
use Illuminate\Contracts\View\View;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;
use Relaticle\Comments\CommentsConfig;
-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;
use Relaticle\Comments\Models\Comment;
-class CommentItem extends Component
+class CommentItem extends Component implements HasForms
{
+ use InteractsWithForms;
use WithFileUploads;
public Comment $comment;
@@ -24,9 +28,11 @@ class CommentItem extends Component
public bool $isReplying = false;
- public string $editBody = '';
+ /** @var array
*/
+ public ?array $editData = [];
- public string $replyBody = '';
+ /** @var array */
+ public ?array $replyData = [];
/** @var array */
public array $replyAttachments = [];
@@ -36,30 +42,60 @@ class CommentItem extends Component
$this->comment = $comment;
}
+ public function editForm(Schema $schema): Schema
+ {
+ return $schema
+ ->components([
+ RichEditor::make('body')
+ ->hiddenLabel()
+ ->required()
+ ->placeholder(__('Edit your comment...'))
+ ->toolbarButtons(CommentsConfig::getEditorToolbar())
+ ->mentions([
+ CommentsConfig::makeMentionProvider(),
+ ]),
+ ])
+ ->statePath('editData');
+ }
+
+ public function replyForm(Schema $schema): Schema
+ {
+ return $schema
+ ->components([
+ RichEditor::make('body')
+ ->hiddenLabel()
+ ->required()
+ ->placeholder(__('Write a reply...'))
+ ->toolbarButtons(CommentsConfig::getEditorToolbar())
+ ->mentions([
+ CommentsConfig::makeMentionProvider(),
+ ]),
+ ])
+ ->statePath('replyData');
+ }
+
public function startEdit(): void
{
$this->authorize('update', $this->comment);
$this->isEditing = true;
- $this->editBody = $this->comment->body;
+ $this->editForm->fill(['body' => $this->comment->body]);
}
public function cancelEdit(): void
{
$this->isEditing = false;
- $this->editBody = '';
+ $this->editForm->fill();
}
public function saveEdit(): void
{
$this->authorize('update', $this->comment);
- $this->validate([
- 'editBody' => ['required', 'string', 'min:1'],
- ]);
+ $data = $this->editForm->getState();
$this->comment->update([
- 'body' => $this->editBody,
+ 'body' => $data['body'] ?? '',
'edited_at' => now(),
]);
@@ -70,7 +106,7 @@ class CommentItem extends Component
$this->dispatch('commentUpdated');
$this->isEditing = false;
- $this->editBody = '';
+ $this->editForm->fill();
}
public function deleteComment(): void
@@ -91,12 +127,13 @@ class CommentItem extends Component
}
$this->isReplying = true;
+ $this->replyForm->fill();
}
public function cancelReply(): void
{
$this->isReplying = false;
- $this->replyBody = '';
+ $this->replyForm->fill();
$this->replyAttachments = [];
}
@@ -104,20 +141,20 @@ class CommentItem extends Component
{
$this->authorize('reply', $this->comment);
- $rules = ['replyBody' => ['required', 'string', 'min:1']];
+ $data = $this->replyForm->getState();
if (CommentsConfig::areAttachmentsEnabled()) {
$maxSize = CommentsConfig::getAttachmentMaxSize();
$allowedTypes = implode(',', CommentsConfig::getAttachmentAllowedTypes());
- $rules['replyAttachments.*'] = ['nullable', 'file', "max:{$maxSize}", "mimetypes:{$allowedTypes}"];
+ $this->validate([
+ 'replyAttachments.*' => ['nullable', 'file', "max:{$maxSize}", "mimetypes:{$allowedTypes}"],
+ ]);
}
- $this->validate($rules);
-
$user = CommentsConfig::resolveAuthenticatedUser();
$reply = $this->comment->commentable->comments()->create([
- 'body' => $this->replyBody,
+ 'body' => $data['body'] ?? '',
'parent_id' => $this->comment->id,
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
@@ -146,7 +183,7 @@ class CommentItem extends Component
$this->dispatch('commentUpdated');
$this->isReplying = false;
- $this->replyBody = '';
+ $this->replyForm->fill();
$this->replyAttachments = [];
}
@@ -157,25 +194,6 @@ class CommentItem extends Component
$this->replyAttachments = array_values($attachments);
}
- /** @return array */
- 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->getCommentDisplayName(),
- 'avatar_url' => $user->getCommentAvatarUrl(),
- ])
- ->values()
- ->all();
- }
-
public function render(): View
{
return view('comments::livewire.comment-item');
diff --git a/src/Livewire/Comments.php b/src/Livewire/Comments.php
index 49156b5..bfbe6bc 100644
--- a/src/Livewire/Comments.php
+++ b/src/Livewire/Comments.php
@@ -2,6 +2,10 @@
namespace Relaticle\Comments\Livewire;
+use Filament\Forms\Components\RichEditor;
+use Filament\Forms\Concerns\InteractsWithForms;
+use Filament\Forms\Contracts\HasForms;
+use Filament\Schemas\Schema;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
@@ -10,19 +14,20 @@ use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;
use Relaticle\Comments\CommentsConfig;
-use Relaticle\Comments\Contracts\MentionResolver;
use Relaticle\Comments\Events\CommentCreated;
use Relaticle\Comments\Mentions\MentionParser;
use Relaticle\Comments\Models\Comment;
use Relaticle\Comments\Models\Subscription;
-class Comments extends Component
+class Comments extends Component implements HasForms
{
+ use InteractsWithForms;
use WithFileUploads;
public Model $model;
- public string $newComment = '';
+ /** @var array */
+ public ?array $commentData = [];
public string $sortDirection = 'asc';
@@ -38,6 +43,23 @@ class Comments extends Component
$this->model = $model;
$this->perPage = CommentsConfig::getPerPage();
$this->loadedCount = $this->perPage;
+ $this->commentForm->fill();
+ }
+
+ public function commentForm(Schema $schema): Schema
+ {
+ return $schema
+ ->components([
+ RichEditor::make('body')
+ ->hiddenLabel()
+ ->required()
+ ->placeholder(__('Write a comment...'))
+ ->toolbarButtons(CommentsConfig::getEditorToolbar())
+ ->mentions([
+ CommentsConfig::makeMentionProvider(),
+ ]),
+ ])
+ ->statePath('commentData');
}
/** @return Collection */
@@ -95,22 +117,22 @@ class Comments extends Component
public function addComment(): void
{
- $rules = ['newComment' => ['required', 'string', 'min:1']];
+ $data = $this->commentForm->getState();
if (CommentsConfig::areAttachmentsEnabled()) {
$maxSize = CommentsConfig::getAttachmentMaxSize();
$allowedTypes = implode(',', CommentsConfig::getAttachmentAllowedTypes());
- $rules['attachments.*'] = ['nullable', 'file', "max:{$maxSize}", "mimetypes:{$allowedTypes}"];
+ $this->validate([
+ 'attachments.*' => ['nullable', 'file', "max:{$maxSize}", "mimetypes:{$allowedTypes}"],
+ ]);
}
- $this->validate($rules);
-
$this->authorize('create', CommentsConfig::getCommentModel());
$user = CommentsConfig::resolveAuthenticatedUser();
$comment = $this->model->comments()->create([
- 'body' => $this->newComment,
+ 'body' => $data['body'] ?? '',
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
]);
@@ -135,7 +157,8 @@ class Comments extends Component
app(MentionParser::class)->syncMentions($comment);
- $this->reset('newComment', 'attachments');
+ $this->commentForm->fill();
+ $this->reset('attachments');
}
public function removeAttachment(int $index): void
@@ -183,25 +206,6 @@ class Comments extends Component
unset($this->comments, $this->totalCount, $this->hasMore);
}
- /** @return array */
- 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->getCommentDisplayName(),
- 'avatar_url' => $user->getCommentAvatarUrl(),
- ])
- ->values()
- ->all();
- }
-
public function render(): View
{
return view('comments::livewire.comments');
diff --git a/src/Mentions/MentionParser.php b/src/Mentions/MentionParser.php
index 1cf5bdc..f21eb89 100644
--- a/src/Mentions/MentionParser.php
+++ b/src/Mentions/MentionParser.php
@@ -16,6 +16,32 @@ class MentionParser
/** @return Collection */
public function parse(string $body): Collection
+ {
+ $ids = $this->parseRichEditorMentions($body);
+
+ if ($ids->isNotEmpty()) {
+ return $ids;
+ }
+
+ return $this->parsePlainTextMentions($body);
+ }
+
+ /** @return Collection */
+ protected function parseRichEditorMentions(string $body): Collection
+ {
+ preg_match_all('/data-type=["\']mention["\'][^>]*data-id=["\'](\d+)["\']/', $body, $matches);
+
+ if (empty($matches[1])) {
+ preg_match_all('/data-id=["\'](\d+)["\'][^>]*data-type=["\']mention["\']/', $body, $matches);
+ }
+
+ $ids = array_unique(array_map('intval', $matches[1] ?? []));
+
+ return collect($ids);
+ }
+
+ /** @return Collection */
+ protected function parsePlainTextMentions(string $body): Collection
{
$text = html_entity_decode(strip_tags($body), ENT_QUOTES, 'UTF-8');
diff --git a/src/Models/Comment.php b/src/Models/Comment.php
index 9be057b..f519ecf 100644
--- a/src/Models/Comment.php
+++ b/src/Models/Comment.php
@@ -2,6 +2,8 @@
namespace Relaticle\Comments\Models;
+use Filament\Forms\Components\RichEditor\MentionProvider;
+use Filament\Forms\Components\RichEditor\RichContentRenderer;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -145,6 +147,19 @@ class Comment extends Model
public function renderBodyWithMentions(): string
{
$body = $this->body;
+
+ if ($this->hasRichEditorMentions($body)) {
+ return RichContentRenderer::make($body)
+ ->mentions([
+ MentionProvider::make('@')
+ ->getLabelsUsing(fn (array $ids): array => CommentsConfig::getCommenterModel()::query()
+ ->whereIn('id', $ids)
+ ->pluck('name', 'id')
+ ->all()),
+ ])
+ ->toHtml();
+ }
+
$mentionNames = $this->mentions->pluck('name')->filter()->unique();
foreach ($mentionNames as $name) {
@@ -157,4 +172,9 @@ class Comment extends Model
return $body;
}
+
+ protected function hasRichEditorMentions(string $body): bool
+ {
+ return str_contains($body, 'data-type="mention"') || str_contains($body, '') || str_contains($body, '