implement sending Emoji, pin or mute conversation

This commit is contained in:
2026-05-01 11:30:14 +03:30
parent 1121939c25
commit 7b2541dd35
11 changed files with 359 additions and 12 deletions

View File

@@ -65,7 +65,7 @@ class ConversationDetailsPanel extends Component
->forUser(Auth::user())
->with([
'participants' => fn ($query) => $query
->select(['id', 'conversation_id', 'user_id', 'role', 'joined_at'])
->select(['id', 'conversation_id', 'user_id', 'role', 'joined_at', 'muted_until', 'pinned_at'])
->with('user:id,name,email'),
])
->withCount([
@@ -104,11 +104,73 @@ class ConversationDetailsPanel extends Component
return Gate::allows('addMembers', $this->conversation);
}
public function currentParticipant(): ConversationParticipant
{
$participant = $this->conversation->participants
->first(fn (ConversationParticipant $participant) => $participant->user_id === Auth::id());
abort_unless($participant instanceof ConversationParticipant, 404);
return $participant;
}
public function isMuted(): bool
{
return $this->currentParticipant()->isMuted();
}
public function isPinned(): bool
{
return $this->currentParticipant()->isPinned();
}
public function closeDetails(): void
{
$this->dispatch('conversation-details-toggled');
}
public function toggleMute(): void
{
Gate::authorize('view', $this->conversation);
$participant = $this->currentParticipant();
$wasMuted = $participant->isMuted();
$participant->forceFill([
'muted_until' => $wasMuted ? null : now()->addYear(),
])->save();
unset($this->conversation);
Flux::toast(
variant: 'success',
text: $wasMuted ? __('Conversation unmuted.') : __('Conversation muted.'),
);
$this->dispatch('conversation-updated', conversationId: $this->conversationId);
}
public function togglePin(): void
{
Gate::authorize('view', $this->conversation);
$participant = $this->currentParticipant();
$wasPinned = $participant->isPinned();
$participant->forceFill([
'pinned_at' => $wasPinned ? null : now(),
])->save();
unset($this->conversation);
Flux::toast(
variant: 'success',
text: $wasPinned ? __('Conversation unpinned.') : __('Conversation pinned.'),
);
$this->dispatch('conversation-updated', conversationId: $this->conversationId);
}
public function openAddMembers(): void
{
Gate::authorize('addMembers', $this->conversation);

View File

@@ -196,12 +196,19 @@ class ConversationList extends Component
->forUser($user)
->with([
'participants' => fn ($query) => $query
->select(['id', 'conversation_id', 'user_id', 'role', 'last_read_at'])
->select(['id', 'conversation_id', 'user_id', 'role', 'last_read_at', 'muted_until', 'pinned_at'])
->with('user:id,name,email'),
'latestMessage' => fn ($query) => $query
->select(['messages.id', 'messages.conversation_id', 'messages.user_id', 'messages.type', 'messages.body', 'messages.metadata', 'messages.created_at']),
'latestMessage.sender:id,name,email',
])
->addSelect([
'current_participant_pinned_at' => ConversationParticipant::query()
->select('pinned_at')
->whereColumn('conversation_participants.conversation_id', 'conversations.id')
->where('conversation_participants.user_id', $user->id)
->limit(1),
])
->withMax('messages', 'created_at')
->withCount([
'messages as unread_messages_count' => fn (Builder $messages) => $messages
@@ -224,6 +231,7 @@ class ConversationList extends Component
->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")));
}))
->orderByDesc('current_participant_pinned_at')
->orderByDesc('messages_max_created_at')
->orderByDesc('updated_at')
->limit(40)
@@ -336,6 +344,16 @@ class ConversationList extends Component
]);
}
public function isPinnedFor(Conversation $conversation): bool
{
return (bool) $this->currentParticipantFor($conversation)?->isPinned();
}
public function isMutedFor(Conversation $conversation): bool
{
return (bool) $this->currentParticipantFor($conversation)?->isMuted();
}
public function isOnline(Conversation $conversation): bool
{
$participant = $this->otherParticipant($conversation);
@@ -343,6 +361,12 @@ class ConversationList extends Component
return $participant instanceof User && $participant->id % 3 !== 0;
}
private function currentParticipantFor(Conversation $conversation): ?ConversationParticipant
{
return $conversation->participants
->first(fn (ConversationParticipant $participant) => $participant->user_id === Auth::id());
}
private function otherParticipant(Conversation $conversation): ?User
{
return $conversation->participants

View File

@@ -23,6 +23,8 @@ class MessageComposer extends Component
public string $body = '';
public bool $emojiPickerOpen = false;
/**
* @var array<int, TemporaryUploadedFile>
*/
@@ -103,11 +105,32 @@ class MessageComposer extends Component
});
$this->reset('body', 'attachments');
$this->emojiPickerOpen = false;
$this->resetValidation();
$this->dispatch('message-created', conversationId: $conversation->id, messageId: $message->id);
}
public function toggleEmojiPicker(): void
{
$this->emojiPickerOpen = ! $this->emojiPickerOpen;
}
public function appendEmoji(string $code): void
{
$emoji = $this->emojiForCode($code);
if ($emoji === null) {
return;
}
$body = rtrim($this->body);
$this->body = $body === '' ? $emoji : $body.' '.$emoji;
$this->emojiPickerOpen = false;
$this->resetValidation('body');
}
public function removeAttachment(int $index): void
{
if (! array_key_exists($index, $this->attachments)) {
@@ -122,6 +145,33 @@ class MessageComposer extends Component
$this->resetValidation("attachments.{$index}");
}
/**
* @return array<int, array{code: string, label: string}>
*/
public function emojiOptions(): array
{
return [
['code' => '1F600', 'label' => __('Grinning face')],
['code' => '1F604', 'label' => __('Smiling face')],
['code' => '1F602', 'label' => __('Laughing face')],
['code' => '1F60D', 'label' => __('Heart eyes')],
['code' => '1F44B', 'label' => __('Wave')],
['code' => '1F44D', 'label' => __('Thumbs up')],
['code' => '1F44F', 'label' => __('Clap')],
['code' => '1F64C', 'label' => __('Raised hands')],
['code' => '1F525', 'label' => __('Fire')],
['code' => '2728', 'label' => __('Sparkles')],
['code' => '2705', 'label' => __('Check mark')],
['code' => '1F680', 'label' => __('Rocket')],
['code' => '1F4A1', 'label' => __('Idea')],
['code' => '1F440', 'label' => __('Eyes')],
['code' => '1F4CC', 'label' => __('Pin')],
['code' => '1F4CE', 'label' => __('Paperclip')],
['code' => '2764', 'label' => __('Heart')],
['code' => '1F389', 'label' => __('Party')],
];
}
private function conversation(): Conversation
{
$conversation = Conversation::query()
@@ -140,6 +190,19 @@ class MessageComposer extends Component
->isNotEmpty();
}
private function emojiForCode(string $code): ?string
{
$code = strtoupper($code);
$isAllowed = collect($this->emojiOptions())
->contains(fn (array $emoji): bool => $emoji['code'] === $code);
if (! $isAllowed) {
return null;
}
return html_entity_decode('&#x'.$code.';', ENT_QUOTES, 'UTF-8');
}
public function render(): View
{
return view('livewire.chat.message-composer');