Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,51 @@ By default, Commentions ships with the following reactions: `['👍', '❤️',
],
```

#### Configuring Attachments

Commentions can let users attach files to their comments. The feature is **disabled by default**. Publish the migration that ships with the package (`create_commentions_attachments_table`) before enabling it.

Enable attachments globally in `config/commentions.php` (or via the `COMMENTIONS_ATTACHMENTS_ENABLED` env variable):

```php
'attachments' => [
'enabled' => env('COMMENTIONS_ATTACHMENTS_ENABLED', false),

// Filesystem disk and directory used to store uploads.
'disk' => env('COMMENTIONS_ATTACHMENTS_DISK', 'public'),
'directory' => env('COMMENTIONS_ATTACHMENTS_DIRECTORY', 'commentions-attachments'),

// Maximum size per file, in kilobytes.
'max_size' => (int) env('COMMENTIONS_ATTACHMENTS_MAX_SIZE', 10240),

// Maximum number of files per comment.
'max_files' => (int) env('COMMENTIONS_ATTACHMENTS_MAX_FILES', 5),

// Accepted MIME types, validated against the file's actual contents.
'accepted_mime_types' => [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf',
// ...
],
],
```

You can also toggle attachments per component instead of globally, which overrides the config value:

```php
use Kirschbaum\Commentions\Filament\Infolists\Components\CommentsEntry;

CommentsEntry::make('comments')->enableAttachments();
CommentsEntry::make('comments')->enableAttachments(fn () => auth()->user()->isAdmin());
CommentsEntry::make('comments')->disableAttachments();
```

The same `enableAttachments()` / `disableAttachments()` methods are available on `CommentsAction` and `CommentsTableAction`.

> [!WARNING]
> `accepted_mime_types` ships with a safe set of image and document types. Leaving it empty allows **any** file type, which is dangerous on a `public` disk: types browsers execute in-origin (such as `image/svg+xml` or `text/html`) would be served directly from your application's URL and could be used for stored XSS. Keep an explicit allowlist, or store attachments on a private disk.

Attachments are deleted from both the database and the underlying disk when their parent comment is deleted through the model (`$comment->delete()`).

### Configuring the Commenter name

By default, the `name` property will be used to render the mention names. You can customize it either by implementing the Filament `HasName` interface OR by implementing the optional `getCommenterName` method.
Expand Down
43 changes: 43 additions & 0 deletions config/commentions.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'comments' => 'comments',
'comment_reactions' => 'comment_reactions',
'comment_subscriptions' => 'comment_subscriptions',
'comment_attachments' => 'comment_attachments',
],

/*
Expand Down Expand Up @@ -46,6 +47,48 @@
'allowed' => ['👍', '❤️', '😂', '😮', '😢', '🤔'],
],

/*
|--------------------------------------------------------------------------
| Attachments
|--------------------------------------------------------------------------
|
| File attachments on comments. Disabled by default; enable globally here
| or per component with CommentsEntry::make()->enableAttachments(). Files
| are stored on the configured filesystem disk.
|
*/
'attachments' => [
'enabled' => env('COMMENTIONS_ATTACHMENTS_ENABLED', false),

'disk' => env('COMMENTIONS_ATTACHMENTS_DISK', 'public'),

'directory' => env('COMMENTIONS_ATTACHMENTS_DIRECTORY', 'commentions-attachments'),

// Maximum size per file, in kilobytes.
'max_size' => (int) env('COMMENTIONS_ATTACHMENTS_MAX_SIZE', 10240),

// Maximum number of files per comment.
'max_files' => (int) env('COMMENTIONS_ATTACHMENTS_MAX_FILES', 5),

// Accepted MIME types, validated against the file's actual contents.
// The defaults below cover common images and documents. Setting this
// to an empty array allows ANY file type — avoid this on a public
// disk, since it permits files browsers execute in-origin (such as
// image/svg+xml or text/html) to be served from your app's URL.
'accepted_mime_types' => [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'application/pdf',
'text/plain',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
],

/*
|--------------------------------------------------------------------------
| Subscriptions
Expand Down
27 changes: 27 additions & 0 deletions database/migrations/create_commentions_attachments_table.php.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::create(config('commentions.tables.comment_attachments', 'comment_attachments'), function (Blueprint $table) {
$table->id();
$table->foreignId('comment_id')->constrained(config('commentions.tables.comments'))->cascadeOnDelete();
$table->string('disk');
$table->string('path');
$table->string('filename');
$table->string('mime_type')->nullable();
$table->unsignedBigInteger('size')->nullable();
$table->timestamps();
});
}

public function down(): void
{
Schema::dropIfExists(config('commentions.tables.comment_attachments', 'comment_attachments'));
}
};
2 changes: 1 addition & 1 deletion resources/dist/commentions.css

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions resources/lang/ar/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
'save' => 'حفظ',
'comment' => 'تعليق',
'add_reaction' => 'إضافة رد فعل',
'attach_files' => 'إرفاق ملفات',
'uploading' => 'جارٍ الرفع…',
'show_more' => 'عرض المزيد',

'notifications' => 'الإشعارات',
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/en/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
'save' => 'Save',
'comment' => 'Comment',
'add_reaction' => 'Add Reaction',
'attach_files' => 'Attach files',
'uploading' => 'Uploading…',
'show_more' => 'Show More',

'notifications' => 'Notifications',
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/es/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
'save' => 'Guardar',
'comment' => 'Comentar',
'add_reaction' => 'Agregar reacción',
'attach_files' => 'Adjuntar archivos',
'uploading' => 'Subiendo…',
'show_more' => 'Mostrar más',

'notifications' => 'Notificaciones',
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/fr/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
'save' => 'Enregistrer',
'comment' => 'Commenter',
'add_reaction' => 'Ajouter une réaction',
'attach_files' => 'Joindre des fichiers',
'uploading' => 'Téléversement…',
'show_more' => 'Afficher plus',

'notifications' => 'Notifications',
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/nl/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
'save' => 'Opslaan',
'comment' => 'Opmerking',
'add_reaction' => 'Reactie toevoegen',
'attach_files' => 'Bestanden bijvoegen',
'uploading' => 'Uploaden…',

'notifications' => 'Meldingen',
'unsubscribe' => 'Afmelden',
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/ro/comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
'save' => 'Salvare',
'comment' => 'Comentariu',
'add_reaction' => 'Adaugă reacție',
'attach_files' => 'Atașează fișiere',
'uploading' => 'Se încarcă…',
'show_more' => 'Afișează mai multe',

'notifications' => 'Notificări',
Expand Down
26 changes: 26 additions & 0 deletions resources/views/comment.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ class="comm:text-xs comm:text-gray-300 comm:ml-1"
@else
<div class="comm:mt-1 comm:space-y-6 comm:text-sm comm:text-gray-800 comm:dark:text-gray-200">{!! $comment->getParsedBody() !!}</div>

@if ($comment->isComment() && $comment->attachments->isNotEmpty())
<div class="comm:mt-2 comm:flex comm:flex-wrap comm:gap-2">
@foreach ($comment->attachments as $attachment)
@if ($attachment->isImage())
<a href="{{ $attachment->getUrl() }}" target="_blank" rel="noopener">
<img
src="{{ $attachment->getUrl() }}"
alt="{{ $attachment->filename }}"
class="comm:h-20 comm:w-20 comm:rounded-lg comm:border comm:border-gray-200 comm:dark:border-gray-700 comm:object-cover"
/>
</a>
@else
<a
href="{{ $attachment->getUrl() }}"
target="_blank"
rel="noopener"
class="comm:inline-flex comm:items-center comm:gap-1 comm:rounded-lg comm:border comm:border-gray-200 comm:dark:border-gray-700 comm:px-2 comm:py-1 comm:text-xs comm:text-gray-700 comm:dark:text-gray-200 comm:hover:bg-gray-100 comm:dark:hover:bg-gray-800"
>
<x-filament::icon icon="heroicon-s-paper-clip" class="comm:h-4 comm:w-4" />
<span class="comm:max-w-[12rem] comm:truncate">{{ $attachment->filename }}</span>
</a>
@endif
@endforeach
</div>
@endif

@if ($comment->isComment())
<livewire:dynamic-component
:component="$commentionsComponentPrefix . 'reactions'"
Expand Down
1 change: 1 addition & 0 deletions resources/views/comments-modal.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
:sidebar-enabled="$sidebarEnabled ?? true"
:show-subscribers="$showSubscribers ?? true"
:tip-tap-css-classes="$tipTapCssClasses ?? null"
:attachments-enabled="$attachmentsEnabled ?? null"
/>
</div>
39 changes: 39 additions & 0 deletions resources/views/comments.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,45 @@
</div>
</div>

@if ($this->attachmentsAreEnabled())
<div class="comm:mb-2 comm:space-y-1" x-show="wasFocused" x-cloak>
<label class="comm:inline-flex comm:cursor-pointer comm:items-center comm:gap-1 comm:rounded-lg comm:border comm:border-gray-300 comm:dark:border-gray-700 comm:px-2 comm:py-1 comm:text-xs comm:text-gray-600 comm:dark:text-gray-300 comm:hover:bg-gray-100 comm:dark:hover:bg-gray-800">
<input type="file" class="comm:hidden" wire:model="attachments" multiple />
<x-filament::icon icon="heroicon-s-paper-clip" class="comm:h-4 comm:w-4" />
<span>{{ __('commentions::comments.attach_files') }}</span>
</label>

<div wire:loading wire:target="attachments" class="comm:text-xs comm:text-gray-500">
{{ __('commentions::comments.uploading') }}
</div>

@error('attachments')
<p class="comm:text-xs comm:text-red-600">{{ $message }}</p>
@enderror
@error('attachments.*')
<p class="comm:text-xs comm:text-red-600">{{ $message }}</p>
@enderror

@if (! empty($attachments))
<ul class="comm:space-y-1">
@foreach ($attachments as $attachmentIndex => $pendingAttachment)
<li class="comm:flex comm:items-center comm:gap-1.5 comm:text-xs comm:text-gray-600 comm:dark:text-gray-300">
<x-filament::icon icon="heroicon-s-document" class="comm:h-4 comm:w-4 comm:flex-shrink-0" />
<span class="comm:truncate">{{ $pendingAttachment->getClientOriginalName() }}</span>
<button
type="button"
wire:click="removeAttachment({{ $attachmentIndex }})"
class="comm:text-red-600 comm:hover:text-red-700"
>
<x-filament::icon icon="heroicon-s-x-mark" class="comm:h-4 comm:w-4" />
</button>
</li>
@endforeach
</ul>
@endif
</div>
@endif

<template x-if="wasFocused">
<div>
<x-filament::button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
:per-page-increment="$getPerPageIncrement()"
:sidebar-enabled="$isSidebarEnabled()"
:tip-tap-css-classes="$getTipTapCssClasses()"
:attachments-enabled="$attachmentsAreEnabled()"
/>
</x-dynamic-component>
61 changes: 61 additions & 0 deletions src/Actions/StoreCommentAttachments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Kirschbaum\Commentions\Actions;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Kirschbaum\Commentions\Comment;
use Kirschbaum\Commentions\CommentAttachment;

class StoreCommentAttachments
{
/**
* Persist uploaded files as attachments for the given comment.
*
* @param array<mixed> $files
* @return array<CommentAttachment>
*/
public function __invoke(Comment $comment, array $files): array
{
$disk = (string) config('commentions.attachments.disk', 'public');
$directory = (string) config('commentions.attachments.directory', 'commentions-attachments');

$attachments = [];

foreach ($files as $file) {
if (! $file instanceof UploadedFile) {
continue;
}

$path = $file->store($directory, $disk);

if (! is_string($path)) {
Log::warning('Failed to store comment attachment.', [
'comment_id' => $comment->getKey(),
'disk' => $disk,
'filename' => $file->getClientOriginalName(),
]);

continue;
}

$attachments[] = $comment->attachments()->create([
'disk' => $disk,
'path' => $path,
'filename' => $file->getClientOriginalName(),
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
]);
}

return $attachments;
}

/**
* @return array<CommentAttachment>
*/
public static function run(...$args): array
{
return (new self())(...$args);
}
}
12 changes: 12 additions & 0 deletions src/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ public function reactions(): HasMany
return $this->hasMany(CommentReaction::class);
}

public function attachments(): HasMany
{
return $this->hasMany(CommentAttachment::class);
}

public function toggleReaction(string $reaction): void
{
ToggleCommentReaction::run($this, $reaction, Config::resolveAuthenticatedUser());
Expand All @@ -183,6 +188,13 @@ public function getContentHash(): string
]));
}

protected static function booted(): void
{
static::deleting(function (Comment $comment): void {
$comment->attachments()->get()->each->delete();
});
}

protected static function newFactory()
{
return CommentFactory::new();
Expand Down
Loading
Loading