implement sending Emoji, pin or mute conversation
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -6,11 +6,11 @@ use Database\Factories\ConversationFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Fillable(['created_by_id', 'type', 'name', 'description'])]
|
||||
class Conversation extends Model
|
||||
@@ -44,7 +44,7 @@ class Conversation extends Model
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'conversation_participants')
|
||||
->withPivot(['role', 'joined_at', 'last_read_at', 'muted_until'])
|
||||
->withPivot(['role', 'joined_at', 'last_read_at', 'muted_until', 'pinned_at'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ namespace App\Models;
|
||||
use Database\Factories\ConversationParticipantFactory;
|
||||
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;
|
||||
|
||||
#[Fillable(['conversation_id', 'user_id', 'role', 'joined_at', 'last_read_at', 'muted_until'])]
|
||||
#[Fillable(['conversation_id', 'user_id', 'role', 'joined_at', 'last_read_at', 'muted_until', 'pinned_at'])]
|
||||
class ConversationParticipant extends Model
|
||||
{
|
||||
/** @use HasFactory<ConversationParticipantFactory> */
|
||||
@@ -27,9 +27,20 @@ class ConversationParticipant extends Model
|
||||
'joined_at' => 'datetime',
|
||||
'last_read_at' => 'datetime',
|
||||
'muted_until' => 'datetime',
|
||||
'pinned_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function isMuted(): bool
|
||||
{
|
||||
return $this->muted_until !== null && $this->muted_until->isFuture();
|
||||
}
|
||||
|
||||
public function isPinned(): bool
|
||||
{
|
||||
return $this->pinned_at !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Conversation, $this>
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user