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>
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,7 @@ class ConversationParticipantFactory extends Factory
|
||||
'joined_at' => fake()->dateTimeBetween('-2 months', 'now'),
|
||||
'last_read_at' => fake()->optional(0.85)->dateTimeBetween('-2 weeks', 'now'),
|
||||
'muted_until' => null,
|
||||
'pinned_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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::table('conversation_participants', function (Blueprint $table): void {
|
||||
$table->timestamp('pinned_at')->nullable()->after('muted_until');
|
||||
$table->index(['user_id', 'pinned_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('conversation_participants', function (Blueprint $table): void {
|
||||
$table->dropIndex(['user_id', 'pinned_at']);
|
||||
$table->dropColumn('pinned_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -43,12 +43,36 @@
|
||||
<flux:heading size="sm">{{ __('Quick actions') }}</flux:heading>
|
||||
|
||||
<div class="mt-3 grid grid-cols-3 gap-2">
|
||||
<flux:tooltip :content="__('Mute')" position="top">
|
||||
<flux:button type="button" variant="filled" icon="bell-slash" aria-label="{{ __('Mute') }}" />
|
||||
<flux:tooltip :content="$this->isMuted() ? __('Unmute') : __('Mute')" position="top">
|
||||
<flux:button
|
||||
type="button"
|
||||
:variant="$this->isMuted() ? 'primary' : 'filled'"
|
||||
icon="bell-slash"
|
||||
wire:click="toggleMute"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="toggleMute"
|
||||
aria-pressed="{{ $this->isMuted() ? 'true' : 'false' }}"
|
||||
aria-label="{{ $this->isMuted() ? __('Unmute conversation') : __('Mute conversation') }}"
|
||||
class="w-full"
|
||||
>
|
||||
{{ $this->isMuted() ? __('Muted') : __('Mute') }}
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
|
||||
<flux:tooltip :content="__('Pin')" position="top">
|
||||
<flux:button type="button" variant="filled" icon="bookmark" aria-label="{{ __('Pin') }}" />
|
||||
<flux:tooltip :content="$this->isPinned() ? __('Unpin') : __('Pin')" position="top">
|
||||
<flux:button
|
||||
type="button"
|
||||
:variant="$this->isPinned() ? 'primary' : 'filled'"
|
||||
icon="bookmark"
|
||||
wire:click="togglePin"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="togglePin"
|
||||
aria-pressed="{{ $this->isPinned() ? 'true' : 'false' }}"
|
||||
aria-label="{{ $this->isPinned() ? __('Unpin conversation') : __('Pin conversation') }}"
|
||||
class="w-full"
|
||||
>
|
||||
{{ $this->isPinned() ? __('Pinned') : __('Pin') }}
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
|
||||
<flux:tooltip :content="__('Files')" position="top">
|
||||
@@ -62,7 +86,9 @@
|
||||
wire:target="openFiles"
|
||||
aria-label="{{ __('Files') }}"
|
||||
class="w-full"
|
||||
/>
|
||||
>
|
||||
{{ __('Files') }}
|
||||
</flux:button>
|
||||
|
||||
@if ((int) $this->conversation->files_count > 0)
|
||||
<span class="absolute -end-1 -top-1 flex min-w-5 items-center justify-center rounded-full bg-zinc-900 px-1 text-[10px] font-semibold text-white shadow-sm dark:bg-white dark:text-zinc-950">
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
@php
|
||||
$selected = $selectedConversationId === $conversation->id;
|
||||
$unreadCount = (int) ($conversation->unread_messages_count ?? 0);
|
||||
$isPinned = $this->isPinnedFor($conversation);
|
||||
$isMuted = $this->isMutedFor($conversation);
|
||||
@endphp
|
||||
|
||||
<button
|
||||
@@ -111,6 +113,28 @@
|
||||
'bg-sky-50 text-sky-700 dark:bg-sky-400/10 dark:text-sky-300' => ! $selected,
|
||||
])>{{ __('Group') }}</span>
|
||||
@endif
|
||||
|
||||
@if ($isPinned)
|
||||
<span @class([
|
||||
'inline-flex shrink-0 items-center',
|
||||
'text-amber-300 dark:text-amber-600' => $selected,
|
||||
'text-amber-500 dark:text-amber-400' => ! $selected,
|
||||
])>
|
||||
<span class="sr-only">{{ __('Pinned') }}</span>
|
||||
<flux:icon.bookmark class="size-3.5" />
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if ($isMuted)
|
||||
<span @class([
|
||||
'inline-flex shrink-0 items-center',
|
||||
'text-white/60 dark:text-zinc-600' => $selected,
|
||||
'text-zinc-400 dark:text-zinc-500' => ! $selected,
|
||||
])>
|
||||
<span class="sr-only">{{ __('Muted') }}</span>
|
||||
<flux:icon.bell-slash class="size-3.5" />
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<p @class([
|
||||
|
||||
@@ -79,6 +79,27 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@if ($emojiPickerOpen)
|
||||
<div
|
||||
wire:key="emoji-picker"
|
||||
class="mb-2 rounded-lg border border-zinc-200 bg-white p-2 shadow-sm dark:border-zinc-800 dark:bg-zinc-900"
|
||||
>
|
||||
<div class="grid grid-cols-9 gap-1 sm:grid-cols-12">
|
||||
@foreach ($this->emojiOptions() as $emoji)
|
||||
<button
|
||||
type="button"
|
||||
wire:key="emoji-option-{{ $emoji['code'] }}"
|
||||
wire:click="appendEmoji('{{ $emoji['code'] }}')"
|
||||
class="flex aspect-square min-h-9 items-center justify-center rounded-md text-xl transition hover:bg-zinc-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent dark:hover:bg-zinc-800"
|
||||
aria-label="{{ __('Add :emoji emoji', ['emoji' => $emoji['label']]) }}"
|
||||
>
|
||||
{{ html_entity_decode('&#x'.$emoji['code'].';', ENT_QUOTES, 'UTF-8') }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-end gap-2 rounded-lg border border-zinc-200 bg-white p-2 shadow-sm transition focus-within:border-zinc-300 focus-within:ring-2 focus-within:ring-accent focus-within:ring-offset-2 focus-within:ring-offset-white dark:border-zinc-800 dark:bg-zinc-900 dark:focus-within:border-zinc-700 dark:focus-within:ring-offset-zinc-950">
|
||||
<flux:tooltip :content="__('Attach file')" position="top">
|
||||
<flux:button
|
||||
@@ -101,7 +122,14 @@
|
||||
/>
|
||||
|
||||
<flux:tooltip :content="__('Emoji')" position="top">
|
||||
<flux:button type="button" variant="ghost" icon="face-smile" aria-label="{{ __('Emoji') }}" />
|
||||
<flux:button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
icon="face-smile"
|
||||
wire:click="toggleEmojiPicker"
|
||||
aria-label="{{ __('Emoji') }}"
|
||||
aria-expanded="{{ $emojiPickerOpen ? 'true' : 'false' }}"
|
||||
/>
|
||||
</flux:tooltip>
|
||||
|
||||
<flux:button
|
||||
|
||||
@@ -123,6 +123,29 @@ test('participants can send messages', function () {
|
||||
->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('participants can add emojis from the composer picker', function () {
|
||||
$user = User::factory()->create();
|
||||
$conversation = Conversation::factory()
|
||||
->direct()
|
||||
->for($user, 'creator')
|
||||
->create();
|
||||
|
||||
ConversationParticipant::factory()->for($conversation)->for($user)->create();
|
||||
|
||||
$emoji = html_entity_decode('👍', ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(MessageComposer::class, ['conversationId' => $conversation->id])
|
||||
->call('toggleEmojiPicker')
|
||||
->assertSet('emojiPickerOpen', true)
|
||||
->assertSee('Add Thumbs up emoji')
|
||||
->set('body', 'Looks good')
|
||||
->call('appendEmoji', '1F44D')
|
||||
->assertSet('body', 'Looks good '.$emoji)
|
||||
->assertSet('emojiPickerOpen', false);
|
||||
});
|
||||
|
||||
test('participants can send file messages', function () {
|
||||
Storage::fake('local');
|
||||
|
||||
@@ -262,6 +285,67 @@ test('conversation details files action shows shared files', function () {
|
||||
->assertSee('Conversation files');
|
||||
});
|
||||
|
||||
test('participants can mute and pin conversations from details', function () {
|
||||
$user = User::factory()->create();
|
||||
$conversation = Conversation::factory()
|
||||
->direct()
|
||||
->for($user, 'creator')
|
||||
->create();
|
||||
|
||||
$participant = ConversationParticipant::factory()->for($conversation)->for($user)->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(ConversationDetailsPanel::class, ['conversationId' => $conversation->id])
|
||||
->call('toggleMute')
|
||||
->assertSee('Muted')
|
||||
->call('togglePin')
|
||||
->assertSee('Pinned');
|
||||
|
||||
$participant->refresh();
|
||||
|
||||
expect($participant->muted_until)->not->toBeNull()
|
||||
->and($participant->pinned_at)->not->toBeNull();
|
||||
|
||||
Livewire::test(ConversationDetailsPanel::class, ['conversationId' => $conversation->id])
|
||||
->call('toggleMute')
|
||||
->assertSee('Mute')
|
||||
->call('togglePin')
|
||||
->assertSee('Pin');
|
||||
|
||||
$participant->refresh();
|
||||
|
||||
expect($participant->muted_until)->toBeNull()
|
||||
->and($participant->pinned_at)->toBeNull();
|
||||
});
|
||||
|
||||
test('pinned conversations stay at the top of the conversation list', function () {
|
||||
$user = User::factory()->create();
|
||||
$pinnedTeammate = User::factory()->create(['name' => 'Priority Partner']);
|
||||
$regularTeammate = User::factory()->create(['name' => 'Regular Partner']);
|
||||
|
||||
$pinnedConversation = Conversation::factory()
|
||||
->direct()
|
||||
->for($user, 'creator')
|
||||
->create(['updated_at' => now()->subDay()]);
|
||||
|
||||
$regularConversation = Conversation::factory()
|
||||
->direct()
|
||||
->for($user, 'creator')
|
||||
->create(['updated_at' => now()]);
|
||||
|
||||
ConversationParticipant::factory()->for($pinnedConversation)->for($user)->create(['pinned_at' => now()]);
|
||||
ConversationParticipant::factory()->for($pinnedConversation)->for($pinnedTeammate)->create();
|
||||
ConversationParticipant::factory()->for($regularConversation)->for($user)->create();
|
||||
ConversationParticipant::factory()->for($regularConversation)->for($regularTeammate)->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(ConversationList::class)
|
||||
->assertSeeInOrder(['Priority Partner', 'Regular Partner'])
|
||||
->assertSee('Pinned');
|
||||
});
|
||||
|
||||
test('participants can add people to a direct conversation', function () {
|
||||
$user = User::factory()->create();
|
||||
$teammate = User::factory()->create(['name' => 'Mina Partner']);
|
||||
|
||||
Reference in New Issue
Block a user