From ae4d3506c365cfffccbe36c29c95289c1306a6b8 Mon Sep 17 00:00:00 2001 From: 199ocero <199ocero@gmail.com> Date: Mon, 15 Jul 2024 21:27:53 +0800 Subject: [PATCH] improve search query and added service --- README.md | 19 +-- config/filachat.php | 56 ++++++- src/Livewire/ChatList.php | 188 ++++------------------ src/Services/ChatListService.php | 260 +++++++++++++++++++++++++++++++ 4 files changed, 344 insertions(+), 179 deletions(-) create mode 100644 src/Services/ChatListService.php diff --git a/README.md b/README.md index 004441c..0668c7b 100644 --- a/README.md +++ b/README.md @@ -34,24 +34,7 @@ Run the following command to install FilaChat, which will take care of all migra php artisan filachat:install ``` -This is the contents of the published config file: - -```php - true, - 'user_model' => \App\Models\User::class, - 'agent_model' => \App\Models\User::class, - 'sender_name_column' => 'name', - 'receiver_name_column' => 'name', - 'slug' => 'filachat', - 'navigation_icon' => 'heroicon-o-chat-bubble-bottom-center', - 'max_content_width' => \Filament\Support\Enums\MaxWidth::Full, - 'timezone' => 'UTC', -]; - -``` +You can view the full content of the config file here: [config/filachat.php](https://github.com/199ocero/filachat/blob/main/config/filachat.php) > [!NOTE] > This step is optional if you want to enable role restrictions. You only need to create an agent if you want to set up role-based chat support. diff --git a/config/filachat.php b/config/filachat.php index 5dcd1c3..8feb7a2 100644 --- a/config/filachat.php +++ b/config/filachat.php @@ -25,6 +25,31 @@ */ 'user_model' => \App\Models\User::class, + /* + |-------------------------------------------------------------------------- + | User Searchable Columns + |-------------------------------------------------------------------------- + | + | This option specifies the searchable columns for the user model. This is used + | to search for users in the chat. + | + */ + 'user_searchable_columns' => [ + 'name', + 'email', + ], + + /* + |-------------------------------------------------------------------------- + | User Chat List Display Column + |-------------------------------------------------------------------------- + | + | This option specifies the column to be displayed when selecting the user + | in the chat list. + | + */ + 'user_chat_list_display_column' => 'name', + /* |-------------------------------------------------------------------------- | Agent Model @@ -36,13 +61,39 @@ */ 'agent_model' => \App\Models\User::class, + /* + |-------------------------------------------------------------------------- + | Agent Searchable Columns + |-------------------------------------------------------------------------- + | + | This option specifies the searchable columns for the agent model. This is used + | to search for agents in the chat. + | + */ + 'agent_searchable_columns' => [ + 'name', + 'email', + ], + + /* + |-------------------------------------------------------------------------- + | Agent Chat List Display Column + |-------------------------------------------------------------------------- + | + | This option specifies the column to be displayed when selecting the agent + | in the chat list. + | + */ + 'agent_chat_list_display_column' => 'name', + /* |-------------------------------------------------------------------------- | Sender Name Column |-------------------------------------------------------------------------- | | This option specifies the column name for the sender's name. You can - | customize this if your user model uses a different column name. + | customize this if your user model uses a different column name. This also + | use to search for users in the chat. | */ 'sender_name_column' => 'name', @@ -53,7 +104,8 @@ |-------------------------------------------------------------------------- | | This option specifies the column name for the receiver's name. You can - | customize this if your user model uses a different column name. + | customize this if your user model uses a different column name. This also + | use to search for users in the chat. | */ 'receiver_name_column' => 'name', diff --git a/src/Livewire/ChatList.php b/src/Livewire/ChatList.php index 805f066..a6daa55 100644 --- a/src/Livewire/ChatList.php +++ b/src/Livewire/ChatList.php @@ -8,12 +8,8 @@ use Filament\Forms; use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Contracts\HasForms; -use Filament\Notifications\Notification; use Filament\Support\Enums\MaxWidth; -use JaOcero\FilaChat\Events\FilaChatMessageEvent; -use JaOcero\FilaChat\Models\FilaChatConversation; -use JaOcero\FilaChat\Models\FilaChatMessage; -use JaOcero\FilaChat\Pages\FilaChat; +use JaOcero\FilaChat\Services\ChatListService; use Livewire\Attributes\On; use Livewire\Component; @@ -74,88 +70,40 @@ public function createConversationAction(string $name, bool $isLabelHidden = fal } return 'To Agent'; - } else { - return 'To'; } - }) - ->getSearchResultsUsing(function (string $search) use ($isRoleEnabled, $isAgent) { - $authId = auth()->id(); - $searchTerm = '%' . trim($search) . '%'; - - $senderNameColumn = config('filachat.sender_name_column'); - $receiverNameColumn = config('filachat.receiver_name_column'); - - $name = $senderNameColumn ?? $receiverNameColumn; - $userModelClass = config('filachat.user_model'); - $agentModelClass = config('filachat.agent_model'); - - if ($isRoleEnabled) { - - $agentIds = config('filachat.agent_model')::getAllAgentIds(); + return 'To'; + }) + ->placeholder(function () use ($isRoleEnabled, $isAgent) { + if ($isRoleEnabled && ! $isAgent) { + return 'Select Agent by Name or Email'; + } - if ($isAgent) { - return $userModelClass::query() - ->whereNotIn('id', $agentIds) - ->where(function ($query) use ($searchTerm, $senderNameColumn, $receiverNameColumn) { - $query->where($senderNameColumn, 'like', '%' . $searchTerm . '%') - ->orWhere($receiverNameColumn, 'like', '%' . $searchTerm . '%'); - }) - ->get() - ->mapWithKeys(function ($item) use ($name) { - return ['user_' . $item->id => $item->{$name}]; - }); - } + return 'Select User by Name or Email'; + }) + ->searchPrompt(function () use ($isRoleEnabled, $isAgent) { + if ($isRoleEnabled && ! $isAgent) { + return 'Search Agent by Name or Email'; + } - return $agentModelClass::query() - ->whereIn('id', $agentIds) - ->where(function ($query) use ($searchTerm, $senderNameColumn, $receiverNameColumn) { - $query->where($senderNameColumn, 'like', '%' . $searchTerm . '%') - ->orWhere($receiverNameColumn, 'like', '%' . $searchTerm . '%'); - }) - ->get() - ->mapWithKeys(function ($item) use ($name) { - return ['agent_' . $item->id => $item->{$name}]; - }); - } else { - if ($userModelClass === $agentModelClass) { - return $userModelClass::query() - ->whereNot('id', $authId) - ->where(function ($query) use ($searchTerm, $senderNameColumn, $receiverNameColumn) { - $query->where($senderNameColumn, 'like', '%' . $searchTerm . '%') - ->orWhere($receiverNameColumn, 'like', '%' . $searchTerm . '%'); - }) - ->get() - ->mapWithKeys(function ($item) use ($name) { - return ['user_' . $item->id => $item->{$name}]; - }); - } + return 'Search User by Name or Email'; + }) + ->loadingMessage(function () use ($isRoleEnabled, $isAgent) { + if ($isRoleEnabled && ! $isAgent) { + return 'Loading Agents...'; + } - $userModel = $userModelClass::query() - ->whereNot('id', $authId) - ->where(function ($query) use ($searchTerm, $senderNameColumn, $receiverNameColumn) { - $query->where($senderNameColumn, 'like', '%' . $searchTerm . '%') - ->orWhere($receiverNameColumn, 'like', '%' . $searchTerm . '%'); - }) - ->get() - ->mapWithKeys(function ($item) use ($name) { - return ['user_' . $item->id => $item->{$name}]; - }); - - $agentModel = $agentModelClass::query() - ->whereNot('id', $authId) - ->where(function ($query) use ($searchTerm, $senderNameColumn, $receiverNameColumn) { - $query->where($senderNameColumn, 'like', '%' . $searchTerm . '%') - ->orWhere($receiverNameColumn, 'like', '%' . $searchTerm . '%'); - }) - ->get() - ->mapWithKeys(function ($item) use ($name) { - return ['agent_' . $item->id => $item->{$name}]; - }); - - return $userModel->merge($agentModel); + return 'Loading Users...'; + }) + ->noSearchResultsMessage(function () use ($isRoleEnabled, $isAgent) { + if ($isRoleEnabled && ! $isAgent) { + return 'No Agents Found.'; } + + return 'No Users Found.'; }) + ->getSearchResultsUsing(fn (string $search): array => ChatListService::make()->getSearchResults($search)->toArray()) + ->getOptionLabelUsing(fn ($value): ?string => ChatListService::make()->getOptionLabel($value)) ->searchable() ->required(), Forms\Components\Textarea::make('message') @@ -165,85 +113,7 @@ public function createConversationAction(string $name, bool $isLabelHidden = fal ->autosize(), ]) ->modalWidth(MaxWidth::Large) - ->action(function (array $data) { - - try { - $receiverableId = $data['receiverable_id']; - - if (preg_match('/^user_(\d+)$/', $receiverableId, $matches)) { - $receiverableType = config('filachat.user_model'); - $receiverableId = (int) $matches[1]; - } - - if (preg_match('/^agent_(\d+)$/', $receiverableId, $matches)) { - $receiverableType = config('filachat.agent_model'); - $receiverableId = (int) $matches[1]; - } - - $foundConversation = FilaChatConversation::query() - ->where(function ($query) use ($receiverableId, $receiverableType) { - $query->where(function ($query) { - $query->where('senderable_id', auth()->id()) - ->where('senderable_type', auth()->user()::class); - }) - ->orWhere(function ($query) use ($receiverableId, $receiverableType) { - $query->where('senderable_id', $receiverableId) - ->where('senderable_type', $receiverableType); - }); - }) - ->where(function ($query) use ($receiverableId, $receiverableType) { - $query->where(function ($query) use ($receiverableId, $receiverableType) { - $query->where('receiverable_id', $receiverableId) - ->where('receiverable_type', $receiverableType); - }) - ->orWhere(function ($query) { - $query->where('receiverable_id', auth()->id()) - ->where('receiverable_type', auth()->user()::class); - }); - }) - ->first(); - - if (! $foundConversation) { - $conversation = FilaChatConversation::query()->create([ - 'senderable_id' => auth()->id(), - 'senderable_type' => auth()->user()::class, - 'receiverable_id' => $receiverableId, - 'receiverable_type' => $receiverableType, - ]); - } else { - $conversation = $foundConversation; - } - - $message = FilaChatMessage::query()->create([ - 'filachat_conversation_id' => $conversation->id, - 'senderable_id' => auth()->id(), - 'senderable_type' => auth()->user()::class, - 'receiverable_id' => $receiverableId, - 'receiverable_type' => $receiverableType, - 'message' => $data['message'], - ]); - - $conversation->updated_at = now(); - - $conversation->save(); - - broadcast(new FilaChatMessageEvent( - $conversation->id, - $message->id, - $receiverableId, - auth()->id(), - )); - - return $this->redirect(FilaChat::getUrl(tenant: filament()->getTenant()) . '/' . $conversation->id); - } catch (\Exception $exception) { - Notification::make() - ->title('Something went wrong') - ->body($exception->getMessage()) - ->danger() - ->persistent() - ->send(); - } - }); + ->action(fn (array $data) => ChatListService::make()->createConversation($data)); } public function render() diff --git a/src/Services/ChatListService.php b/src/Services/ChatListService.php new file mode 100644 index 0000000..90838a9 --- /dev/null +++ b/src/Services/ChatListService.php @@ -0,0 +1,260 @@ +isRoleEnabled = config('filachat.enable_roles'); + $this->isAgent = auth()->user()->isAgent(); + $this->userModelClass = config('filachat.user_model'); + $this->agentModelClass = config('filachat.agent_model'); + $this->userChatListDisplayColumn = config('filachat.user_chat_list_display_column'); + $this->agentChatListDisplayColumn = config('filachat.agent_chat_list_display_column'); + + // Check if the user model class exists + if (! class_exists($this->userModelClass)) { + throw new InvalidArgumentException('User model class ' . $this->userModelClass . ' not found'); + } + + // Check if the agent model class exists + if (! class_exists($this->agentModelClass)) { + throw new InvalidArgumentException('Agent model class ' . $this->agentModelClass . ' not found'); + } + + // Validate that all specified columns exist in the user model + foreach (config('filachat.user_searchable_columns') as $column) { + $userTable = (new $this->userModelClass)->getTable(); + if (! Schema::hasColumn($userTable, $column)) { + throw new InvalidArgumentException('Column ' . $column . ' not found in ' . $userTable); + } + } + $this->userSearchableColumns = config('filachat.user_searchable_columns'); + + // Validate that all specified columns exist in the agent model + foreach (config('filachat.agent_searchable_columns') as $column) { + $agentTable = (new $this->agentModelClass)->getTable(); + if (! Schema::hasColumn($agentTable, $column)) { + throw new InvalidArgumentException('Column ' . $column . ' not found in ' . $agentTable); + } + } + $this->agentSearchableColumns = config('filachat.agent_searchable_columns'); + } + + public static function make(): self + { + return new self(); + } + + public function getSearchResults(string $search): Collection + { + $searchTerm = '%' . $search . '%'; + + if ($this->isRoleEnabled) { + + $agentIds = $this->agentModelClass::getAllAgentIds(); + + if ($this->isAgent) { + return $this->userModelClass::query() + ->whereNotIn('id', $agentIds) + ->where(function ($query) use ($searchTerm) { + foreach ($this->userSearchableColumns as $column) { + $query->orWhere($column, 'like', $searchTerm); + } + }) + ->select( + DB::raw("CONCAT('user_', id) as user_key"), + DB::raw("$this->userChatListDisplayColumn as user_value") + ) + ->get() + ->pluck('user_value', 'user_key'); + } + + return $this->agentModelClass::query() + ->whereIn('id', $agentIds) + ->where(function ($query) use ($searchTerm) { + foreach ($this->agentSearchableColumns as $column) { + $query->orWhere($column, 'like', $searchTerm); + } + }) + ->select( + DB::raw("CONCAT('agent_', id) as agent_key"), + DB::raw("$this->agentChatListDisplayColumn as agent_value") + ) + ->get() + ->pluck('agent_value', 'agent_key'); + } else { + if ($this->userModelClass === $this->agentModelClass) { + return $this->userModelClass::query() + ->whereNot('id', auth()->id()) + ->where(function ($query) use ($searchTerm) { + foreach ($this->userSearchableColumns as $column) { + $query->orWhere($column, 'like', $searchTerm); + } + }) + ->select( + DB::raw("CONCAT('user_', id) as user_key"), + DB::raw("$this->userChatListDisplayColumn as user_value") + ) + ->get() + ->pluck('user_value', 'user_key'); + } + + $userModel = $this->userModelClass::query() + ->whereNot('id', auth()->id()) + ->where(function ($query) use ($searchTerm) { + foreach ($this->userSearchableColumns as $column) { + $query->orWhere($column, 'like', $searchTerm); + } + }) + ->select( + DB::raw("CONCAT('user_', id) as user_key"), + DB::raw("$this->userChatListDisplayColumn as user_value") + ) + ->get() + ->pluck('user_value', 'user_key'); + + $agentModel = $this->agentModelClass::query() + ->whereNot('id', auth()->id()) + ->where(function ($query) use ($searchTerm) { + foreach ($this->agentSearchableColumns as $column) { + $query->orWhere($column, 'like', $searchTerm); + } + }) + ->select( + DB::raw("CONCAT('agent_', id) as agent_key"), + DB::raw("$this->agentChatListDisplayColumn as agent_value") + ) + ->get() + ->pluck('agent_value', 'agent_key'); + + return $userModel->merge($agentModel); + } + } + + public function getOptionLabel(string $value): ?string + { + if (preg_match('/^user_(\d+)$/', $value, $matches)) { + $id = (int) $matches[1]; + + return $this->userModelClass::find($id)->{$this->userChatListDisplayColumn}; + } + + if (preg_match('/^agent_(\d+)$/', $value, $matches)) { + $id = (int) $matches[1]; + + return $this->agentModelClass::find($id)->{$this->agentChatListDisplayColumn}; + } + + return null; + } + + public function createConversation(array $data) + { + try { + DB::transaction(function () use ($data) { + $receiverableId = $data['receiverable_id']; + + if (preg_match('/^user_(\d+)$/', $receiverableId, $matches)) { + $receiverableType = $this->userModelClass; + $receiverableId = (int) $matches[1]; + } + + if (preg_match('/^agent_(\d+)$/', $receiverableId, $matches)) { + $receiverableType = $this->agentModelClass; + $receiverableId = (int) $matches[1]; + } + + $foundConversation = FilaChatConversation::query() + ->where(function ($query) use ($receiverableId, $receiverableType) { + $query->where(function ($query) { + $query->where('senderable_id', auth()->id()) + ->where('senderable_type', auth()->user()::class); + }) + ->orWhere(function ($query) use ($receiverableId, $receiverableType) { + $query->where('senderable_id', $receiverableId) + ->where('senderable_type', $receiverableType); + }); + }) + ->where(function ($query) use ($receiverableId, $receiverableType) { + $query->where(function ($query) use ($receiverableId, $receiverableType) { + $query->where('receiverable_id', $receiverableId) + ->where('receiverable_type', $receiverableType); + }) + ->orWhere(function ($query) { + $query->where('receiverable_id', auth()->id()) + ->where('receiverable_type', auth()->user()::class); + }); + }) + ->first(); + + if (! $foundConversation) { + $conversation = FilaChatConversation::query()->create([ + 'senderable_id' => auth()->id(), + 'senderable_type' => auth()->user()::class, + 'receiverable_id' => $receiverableId, + 'receiverable_type' => $receiverableType, + ]); + } else { + $conversation = $foundConversation; + } + + $message = FilaChatMessage::query()->create([ + 'filachat_conversation_id' => $conversation->id, + 'senderable_id' => auth()->id(), + 'senderable_type' => auth()->user()::class, + 'receiverable_id' => $receiverableId, + 'receiverable_type' => $receiverableType, + 'message' => $data['message'], + ]); + + $conversation->updated_at = now(); + + $conversation->save(); + + broadcast(new FilaChatMessageEvent( + $conversation->id, + $message->id, + $receiverableId, + auth()->id(), + )); + + return redirect(FilaChat::getUrl(tenant: filament()->getTenant()) . '/' . $conversation->id); + }); + } catch (\Exception $exception) { + Notification::make() + ->title('Something went wrong') + ->body($exception->getMessage()) + ->danger() + ->persistent() + ->send(); + } + } +}