implement uploading file
This commit is contained in:
26
app/Http/Controllers/Chat/MessageAttachmentController.php
Normal file
26
app/Http/Controllers/Chat/MessageAttachmentController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Chat;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Message;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class MessageAttachmentController extends Controller
|
||||
{
|
||||
public function __invoke(Message $message): StreamedResponse
|
||||
{
|
||||
$message->loadMissing('conversation');
|
||||
|
||||
Gate::authorize('view', $message->conversation);
|
||||
|
||||
$path = $message->attachmentPath();
|
||||
|
||||
abort_unless($message->isFile() && $path, 404);
|
||||
abort_unless(Storage::disk($message->attachmentDisk())->exists($path), 404);
|
||||
|
||||
return Storage::disk($message->attachmentDisk())->download($path, $message->attachmentName());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire\Chat;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\ConversationParticipant;
|
||||
use App\Models\Message;
|
||||
use App\Models\User;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Contracts\View\View;
|
||||
@@ -25,6 +26,8 @@ class ConversationDetailsPanel extends Component
|
||||
|
||||
public bool $showAddMembersModal = false;
|
||||
|
||||
public bool $showFilesModal = false;
|
||||
|
||||
public string $memberSearch = '';
|
||||
|
||||
/**
|
||||
@@ -47,6 +50,14 @@ class ConversationDetailsPanel extends Component
|
||||
}
|
||||
}
|
||||
|
||||
#[On('message-created')]
|
||||
public function refreshFiles(int $conversationId): void
|
||||
{
|
||||
if ($conversationId === $this->conversationId) {
|
||||
unset($this->conversation, $this->files);
|
||||
}
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function conversation(): Conversation
|
||||
{
|
||||
@@ -57,7 +68,10 @@ class ConversationDetailsPanel extends Component
|
||||
->select(['id', 'conversation_id', 'user_id', 'role', 'joined_at'])
|
||||
->with('user:id,name,email'),
|
||||
])
|
||||
->withCount('messages')
|
||||
->withCount([
|
||||
'messages',
|
||||
'messages as files_count' => fn (Builder $messages) => $messages->where('type', Message::TypeFile),
|
||||
])
|
||||
->findOrFail($this->conversationId);
|
||||
|
||||
Gate::authorize('view', $conversation);
|
||||
@@ -104,6 +118,15 @@ class ConversationDetailsPanel extends Component
|
||||
$this->showAddMembersModal = true;
|
||||
}
|
||||
|
||||
public function openFiles(): void
|
||||
{
|
||||
Gate::authorize('view', $this->conversation);
|
||||
|
||||
unset($this->files);
|
||||
|
||||
$this->showFilesModal = true;
|
||||
}
|
||||
|
||||
public function toggleMember(int $userId): void
|
||||
{
|
||||
if ($userId === Auth::id()) {
|
||||
@@ -233,6 +256,23 @@ class ConversationDetailsPanel extends Component
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Message>
|
||||
*/
|
||||
#[Computed]
|
||||
public function files(): Collection
|
||||
{
|
||||
Gate::authorize('view', $this->conversation);
|
||||
|
||||
return Message::query()
|
||||
->where('conversation_id', $this->conversationId)
|
||||
->where('type', Message::TypeFile)
|
||||
->with('sender:id,name,email')
|
||||
->latest()
|
||||
->limit(50)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function suggestedGroupName(?Collection $newMemberIds = null): string
|
||||
{
|
||||
$participantNames = $this->conversation->participants
|
||||
|
||||
@@ -199,7 +199,7 @@ class ConversationList extends Component
|
||||
->select(['id', 'conversation_id', 'user_id', 'role', 'last_read_at'])
|
||||
->with('user:id,name,email'),
|
||||
'latestMessage' => fn ($query) => $query
|
||||
->select(['messages.id', 'messages.conversation_id', 'messages.user_id', 'messages.body', 'messages.created_at']),
|
||||
->select(['messages.id', 'messages.conversation_id', 'messages.user_id', 'messages.type', 'messages.body', 'messages.metadata', 'messages.created_at']),
|
||||
'latestMessage.sender:id,name,email',
|
||||
])
|
||||
->withMax('messages', 'created_at')
|
||||
@@ -298,7 +298,11 @@ class ConversationList extends Component
|
||||
? __('You: ')
|
||||
: ($conversation->isGroup() ? $conversation->latestMessage->sender?->name.': ' : '');
|
||||
|
||||
return str($prefix.$conversation->latestMessage->body)
|
||||
$preview = $conversation->latestMessage->isFile()
|
||||
? __('Sent a file: :name', ['name' => $conversation->latestMessage->attachmentName()])
|
||||
: $conversation->latestMessage->body;
|
||||
|
||||
return str($prefix.$preview)
|
||||
->squish()
|
||||
->limit(86)
|
||||
->toString();
|
||||
|
||||
@@ -7,17 +7,27 @@ use App\Models\ConversationParticipant;
|
||||
use App\Models\Message;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class MessageComposer extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
#[Locked]
|
||||
public int $conversationId;
|
||||
|
||||
public string $body = '';
|
||||
|
||||
/**
|
||||
* @var array<int, TemporaryUploadedFile>
|
||||
*/
|
||||
public array $attachments = [];
|
||||
|
||||
public function mount(int $conversationId): void
|
||||
{
|
||||
$this->conversationId = $conversationId;
|
||||
@@ -28,37 +38,90 @@ class MessageComposer extends Component
|
||||
public function sendMessage(): void
|
||||
{
|
||||
$this->body = trim($this->body);
|
||||
$hasAttachments = $this->hasAttachments();
|
||||
|
||||
$validated = $this->validate([
|
||||
'body' => ['required', 'string', 'max:4000'],
|
||||
'body' => [$hasAttachments ? 'nullable' : 'required', 'string', 'max:4000'],
|
||||
'attachments' => ['array', 'max:5'],
|
||||
'attachments.*' => ['file', 'max:10240'],
|
||||
], [
|
||||
'body.required' => __('Write a message before sending.'),
|
||||
'body.required' => __('Write a message or attach a file before sending.'),
|
||||
'attachments.max' => __('Attach up to :max files at a time.'),
|
||||
'attachments.*.file' => __('Each attachment must be a valid file.'),
|
||||
'attachments.*.max' => __('Each attachment must be 10 MB or smaller.'),
|
||||
]);
|
||||
|
||||
$conversation = $this->conversation();
|
||||
|
||||
Gate::authorize('sendMessage', $conversation);
|
||||
|
||||
$message = Message::query()->create([
|
||||
'conversation_id' => $conversation->id,
|
||||
'user_id' => Auth::id(),
|
||||
'type' => Message::TypeText,
|
||||
'body' => $validated['body'],
|
||||
]);
|
||||
$message = DB::transaction(function () use ($conversation, $validated): Message {
|
||||
$latestMessage = null;
|
||||
$body = trim((string) ($validated['body'] ?? ''));
|
||||
|
||||
$conversation->touch();
|
||||
if ($body !== '') {
|
||||
$latestMessage = Message::query()->create([
|
||||
'conversation_id' => $conversation->id,
|
||||
'user_id' => Auth::id(),
|
||||
'type' => Message::TypeText,
|
||||
'body' => $body,
|
||||
]);
|
||||
}
|
||||
|
||||
ConversationParticipant::query()
|
||||
->where('conversation_id', $conversation->id)
|
||||
->where('user_id', Auth::id())
|
||||
->update(['last_read_at' => now()]);
|
||||
foreach ($this->attachments as $attachment) {
|
||||
$metadata = [
|
||||
'disk' => 'local',
|
||||
'original_name' => $attachment->getClientOriginalName(),
|
||||
'mime_type' => $attachment->getMimeType(),
|
||||
'size' => $attachment->getSize(),
|
||||
];
|
||||
|
||||
$this->reset('body');
|
||||
$path = $attachment->store(
|
||||
path: "chat-attachments/{$conversation->id}",
|
||||
options: 'local',
|
||||
);
|
||||
|
||||
$metadata['path'] = $path;
|
||||
|
||||
$latestMessage = Message::query()->create([
|
||||
'conversation_id' => $conversation->id,
|
||||
'user_id' => Auth::id(),
|
||||
'type' => Message::TypeFile,
|
||||
'body' => $metadata['original_name'],
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
}
|
||||
|
||||
$conversation->touch();
|
||||
|
||||
ConversationParticipant::query()
|
||||
->where('conversation_id', $conversation->id)
|
||||
->where('user_id', Auth::id())
|
||||
->update(['last_read_at' => now()]);
|
||||
|
||||
return $latestMessage;
|
||||
});
|
||||
|
||||
$this->reset('body', 'attachments');
|
||||
$this->resetValidation();
|
||||
|
||||
$this->dispatch('message-created', conversationId: $conversation->id, messageId: $message->id);
|
||||
}
|
||||
|
||||
public function removeAttachment(int $index): void
|
||||
{
|
||||
if (! array_key_exists($index, $this->attachments)) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($this->attachments[$index]);
|
||||
|
||||
$this->attachments = array_values($this->attachments);
|
||||
|
||||
$this->resetValidation('attachments');
|
||||
$this->resetValidation("attachments.{$index}");
|
||||
}
|
||||
|
||||
private function conversation(): Conversation
|
||||
{
|
||||
$conversation = Conversation::query()
|
||||
@@ -70,6 +133,13 @@ class MessageComposer extends Component
|
||||
return $conversation;
|
||||
}
|
||||
|
||||
private function hasAttachments(): bool
|
||||
{
|
||||
return collect($this->attachments)
|
||||
->filter()
|
||||
->isNotEmpty();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.chat.message-composer');
|
||||
|
||||
@@ -5,8 +5,9 @@ namespace App\Models;
|
||||
use Database\Factories\MessageFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
#[Fillable(['conversation_id', 'user_id', 'type', 'body', 'metadata', 'edited_at'])]
|
||||
class Message extends Model
|
||||
@@ -16,6 +17,8 @@ class Message extends Model
|
||||
|
||||
public const string TypeText = 'text';
|
||||
|
||||
public const string TypeFile = 'file';
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
@@ -50,4 +53,51 @@ class Message extends Model
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function isFile(): bool
|
||||
{
|
||||
return $this->type === self::TypeFile;
|
||||
}
|
||||
|
||||
public function attachmentDisk(): string
|
||||
{
|
||||
return (string) data_get($this->metadata, 'disk', 'local');
|
||||
}
|
||||
|
||||
public function attachmentPath(): ?string
|
||||
{
|
||||
$path = data_get($this->metadata, 'path');
|
||||
|
||||
return is_string($path) && $path !== '' ? $path : null;
|
||||
}
|
||||
|
||||
public function attachmentName(): string
|
||||
{
|
||||
$name = data_get($this->metadata, 'original_name');
|
||||
|
||||
if (is_string($name) && $name !== '') {
|
||||
return $name;
|
||||
}
|
||||
|
||||
return $this->body ?: __('Attachment');
|
||||
}
|
||||
|
||||
public function attachmentMimeType(): ?string
|
||||
{
|
||||
$mimeType = data_get($this->metadata, 'mime_type');
|
||||
|
||||
return is_string($mimeType) && $mimeType !== '' ? $mimeType : null;
|
||||
}
|
||||
|
||||
public function attachmentSize(): int
|
||||
{
|
||||
return (int) data_get($this->metadata, 'size', 0);
|
||||
}
|
||||
|
||||
public function formattedAttachmentSize(): string
|
||||
{
|
||||
$size = $this->attachmentSize();
|
||||
|
||||
return $size > 0 ? Number::fileSize($size) : __('Unknown size');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user