feature: add chat search
- first check if the chat is available locally, if not then hit the server
This commit is contained in:
parent
2cecc4aceb
commit
7f80d661bd
@ -9,10 +9,14 @@
|
||||
|
||||
final readonly class GetUserChatsAction
|
||||
{
|
||||
public function chats(User $user): LengthAwarePaginator|AbstractPaginator
|
||||
public function chats(User $user, ?string $searchKeyword = null): LengthAwarePaginator|AbstractPaginator
|
||||
{
|
||||
$chats = $user->chats()->latest()->paginate();
|
||||
$chats = $user->chats()->getQuery();
|
||||
if ($searchKeyword) {
|
||||
$chats = $chats->where('title', 'LIKE', "%{$searchKeyword}%");
|
||||
}
|
||||
$results = $chats->latest()->paginate();
|
||||
|
||||
return $chats->through(fn ($chat) => ChatResponseDto::fromModel($chat));
|
||||
return $results->through(fn ($chat) => ChatResponseDto::fromModel($chat));
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,13 +15,11 @@ class ChatController extends Controller
|
||||
{
|
||||
public function index(Request $request, GetUserChatsAction $getUserChatsAction)
|
||||
{
|
||||
return ChatResponseResource::collection($getUserChatsAction->chats($request->user()));
|
||||
return ChatResponseResource::collection($getUserChatsAction->chats($request->user(), $request->input('search', null)));
|
||||
}
|
||||
|
||||
public function store(CreateChatRequest $request, CreateChatAction $createChatAction)
|
||||
{
|
||||
return new ChatResponseResource(
|
||||
$createChatAction->create($request->user(), $request->input('title', 'New Chat'))
|
||||
);
|
||||
return new ChatResponseResource($createChatAction->create($request->user(), $request->input('title', 'New Chat')));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { Header } from './core/layout/header/header';
|
||||
import { Sidebar } from './core/layout/sidebar/sidebar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, Header, Sidebar],
|
||||
imports: [RouterOutlet],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.css',
|
||||
})
|
||||
|
||||
@ -27,4 +27,8 @@ export class ChatService {
|
||||
public getChats() {
|
||||
return this.http.get<ChatCollection>(`${this.apiUrl}/chats`);
|
||||
}
|
||||
|
||||
public searchChat(keyword: string) {
|
||||
return this.http.get<ChatCollection>(`${this.apiUrl}/chats?search=${keyword}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
|
||||
import { inject } from '@angular/core';
|
||||
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
|
||||
import { computed, inject } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { ChatService } from './chat-service';
|
||||
import { MessageState, Message, MessageResponse, ChatResponse, ChatState } from './chat.types';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import {
|
||||
MessageState,
|
||||
Message,
|
||||
MessageResponse,
|
||||
ChatResponse,
|
||||
ChatState,
|
||||
Chat,
|
||||
} from './chat.types';
|
||||
import { debounceTime, distinctUntilChanged, lastValueFrom, of, pipe, switchMap } from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import { JsonApiResource } from '../core/types/api';
|
||||
import { environment } from '../../environments/environment.development';
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
|
||||
const initialMessageState: MessageState = {
|
||||
messages: [
|
||||
@ -30,8 +38,11 @@ const initialMessageState: MessageState = {
|
||||
};
|
||||
|
||||
const initalChatState: ChatState = {
|
||||
chats: [],
|
||||
_chats: [],
|
||||
_serverSearchResults: [],
|
||||
_filteredChats: [],
|
||||
isLoading: true,
|
||||
searchQuery: null,
|
||||
};
|
||||
|
||||
export const MessageStore = signalStore(
|
||||
@ -216,6 +227,12 @@ export const MessageStore = signalStore(
|
||||
export const ChatStore = signalStore(
|
||||
{ providedIn: 'root' },
|
||||
withState(initalChatState),
|
||||
withComputed((store) => ({
|
||||
displayedChats: computed(() => {
|
||||
if (!store.searchQuery()) return store._chats();
|
||||
return store._filteredChats();
|
||||
}),
|
||||
})),
|
||||
withMethods((store) => {
|
||||
const chatService = inject(ChatService);
|
||||
return {
|
||||
@ -225,12 +242,44 @@ export const ChatStore = signalStore(
|
||||
const chats = await lastValueFrom(chatService.getChats());
|
||||
patchState(store, {
|
||||
isLoading: false,
|
||||
chats: chats.data,
|
||||
_chats: chats.data,
|
||||
});
|
||||
} catch (error: any) {
|
||||
patchState(store, { isLoading: false });
|
||||
}
|
||||
},
|
||||
// This method first searches local chats which are loaded on init.
|
||||
// If not hit, then it calls backend for results as chats come paginated.
|
||||
performSearch: rxMethod<string>(
|
||||
pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
switchMap(async (query) => {
|
||||
patchState(store, { isLoading: true });
|
||||
|
||||
if (!query) {
|
||||
patchState(store, { searchQuery: null, _filteredChats: [], isLoading: false });
|
||||
return of(null);
|
||||
}
|
||||
|
||||
patchState(store, { searchQuery: query });
|
||||
|
||||
const localResults = store._chats().filter((chat) => {
|
||||
return chat.attributes.title?.toLowerCase().includes(query.toLowerCase());
|
||||
});
|
||||
if (localResults.length > 0) {
|
||||
patchState(store, {
|
||||
_filteredChats: localResults,
|
||||
});
|
||||
} else {
|
||||
const serverResults = await lastValueFrom(chatService.searchChat(query));
|
||||
patchState(store, { _serverSearchResults: serverResults.data });
|
||||
}
|
||||
patchState(store, { isLoading: false });
|
||||
return of(null);
|
||||
}),
|
||||
),
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@ -30,6 +30,9 @@ export type MessageState = {
|
||||
id: string | null;
|
||||
};
|
||||
export type ChatState = {
|
||||
chats: JsonApiResource<Chat>[];
|
||||
_chats: JsonApiResource<Chat>[];
|
||||
_serverSearchResults: JsonApiResource<Chat>[];
|
||||
_filteredChats: JsonApiResource<Chat>[];
|
||||
isLoading: boolean;
|
||||
searchQuery: string | null;
|
||||
};
|
||||
|
||||
@ -103,11 +103,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (chatStore.chats().length !== 0) {
|
||||
@if (chatStore.displayedChats().length !== 0) {
|
||||
<div class="flex-1 overflow-y-auto px-2 pb-4 scrollbar-thin">
|
||||
<!-- Chat Items -->
|
||||
<div class="space-y-0.5">
|
||||
@for (chat of chatStore.chats(); track chat.id) {
|
||||
@for (chat of chatStore.displayedChats(); track chat.id) {
|
||||
<button (click)="selectChat(chat.id)" [class]="chatItemClasses(chat)">
|
||||
<!-- Active indicator -->
|
||||
@if(activeChatId() && activeChatId() === chat.id){
|
||||
@ -158,7 +158,7 @@
|
||||
</div>
|
||||
}
|
||||
<!-- Empty state -->
|
||||
@if (chatStore.chats().length === 0) {
|
||||
@if (!chatStore.isLoading() && chatStore.displayedChats().length === 0) {
|
||||
<div class="flex flex-1 flex-col items-center justify-center py-10 px-4 text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-[#111a2e] flex items-center justify-center mb-3">
|
||||
<svg
|
||||
|
||||
@ -7,8 +7,7 @@ import { InitialsPipe } from './initials-pipe';
|
||||
import { JsonApiResource } from '../../types/api';
|
||||
import { Chat } from '../../../chat/chat.types';
|
||||
import { Login } from '../../../auth/login/login';
|
||||
import { Loader } from "../../../shared/loader/loader";
|
||||
|
||||
import { Loader } from '../../../shared/loader/loader';
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
standalone: true,
|
||||
@ -24,7 +23,6 @@ export class Sidebar implements OnInit {
|
||||
|
||||
protected isOpen = signal(true);
|
||||
protected isCogMenuOpen = signal(false);
|
||||
protected searchQuery = signal('');
|
||||
protected activeChatId = signal<string | null>(null);
|
||||
|
||||
ngOnInit() {
|
||||
@ -78,7 +76,7 @@ export class Sidebar implements OnInit {
|
||||
}
|
||||
|
||||
protected onSearch(event: Event): void {
|
||||
this.searchQuery.set((event.target as HTMLInputElement).value);
|
||||
this.chatStore.performSearch((event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
protected formatTime(dateValue: string | Date): string {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user