Skip to content

Commit

Permalink
🚧basic chat setup compted
Browse files Browse the repository at this point in the history
  • Loading branch information
sinanptm committed Sep 24, 2024
1 parent 22d8484 commit 78e7a38
Show file tree
Hide file tree
Showing 12 changed files with 105 additions and 74 deletions.
32 changes: 24 additions & 8 deletions client/app/(landing-pages)/chats/@chat/[chatId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
'use client'
import ChatSection from "@/components/page-components/chat/ChatSection"
import { useGetPatientMessages } from "@/lib/hooks/chat/useChatPatient"
import { toast } from "@/components/ui/use-toast"
import { useCreateMessagePatient, useGetPatientMessages } from "@/lib/hooks/chat/useChatPatient"
import { useParams } from "next/navigation"
import { useState } from "react"

const Page = () => {
const chatId = useParams().chatId as string;
const { data:messages, isError, error, isLoading } = useGetPatientMessages(chatId, 10)
const [limit,setLimit] = useState(40)
const { data: response, isError, error, isLoading, refetch } = useGetPatientMessages(chatId, limit);
const { mutate: createMessage, isPending } = useCreateMessagePatient()

const handleSendMessage = (newMessage: string) => {
console.log("New message sent:", newMessage);
}

createMessage({ doctorId: response?.chat.doctorId!, chatId, message: newMessage }, {
onSuccess: () => {
refetch();
},
onError: ({ response }) => {
toast({
title: "Error in sending message ❌",
description: response?.data.message || "Unknown error Occurred",
variant: "destructive"
});
}
})
}

return (
<ChatSection
chatId={chatId}
isDoctor={false}
sender={"doctor"}
isLoading={isLoading}
messages={messages?.items!}
isPending={isPending}
messages={response?.data.items!}
isError={isError}
chat={response?.chat!}
error={error?.response?.data.message}
onSendMessage={handleSendMessage}
/>
Expand Down
21 changes: 16 additions & 5 deletions client/app/(landing-pages)/chats/@chat/default.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { MessageSquare } from "lucide-react"
import { Card, CardContent } from "@/components/ui/card"

export default function ChatDefault() {
return (
<div className="flex items-center justify-center h-full">
<h2>Select a chat to start messaging.</h2>
</div>
);
return (
<div className="flex items-center justify-center h-full bg-gradient-to-br from-background to-secondary/20">
<Card className="w-full max-w-md mx-4">
<CardContent className="flex flex-col items-center text-center p-6 space-y-4">
<MessageSquare className="h-16 w-16 text-primary opacity-80" />
<h2 className="text-2xl font-semibold tracking-tight">Welcome to Your Chat</h2>
<p className="text-muted-foreground">
Select an existing conversation from the sidebar or start a new chat to begin messaging.
</p>
</CardContent>
</Card>
</div>
)
}
12 changes: 7 additions & 5 deletions client/app/(landing-pages)/chats/@chatList/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { useState } from "react";

const Page = () => {
const { data } = useGetDoctorsList();
const { mutate: createChat } = useCreateChatPatient();
const {data:chats,isLoading} = useGetPatientChats()
const { mutate: createChat, } = useCreateChatPatient();
const { data: chats, isLoading, refetch } = useGetPatientChats()
const [isNewChatModalOpen, setNewChatModalOpen] = useState(false)
const router = useRouter();
const doctors: ChatModelUser[] = data?.items.map(({ _id, image, name }) => ({ _id, name, profilePicture: image })) || [];



const handleSelectChat = (id: string) => {
router.push(`/chats/${id}`);
Expand All @@ -29,6 +29,7 @@ const Page = () => {
createChat({ doctorId }, {
onSuccess: ({ chatId }) => {
router.push(`/chats/${chatId}`)
refetch();
setNewChatModalOpen(false)
},
onError: ({ response }) => {
Expand All @@ -45,8 +46,9 @@ const Page = () => {
return (
<>
<ChatList
chats={chats}
isDoctorData={false}
chats={chats!}
sender="doctor"
skeltonCount={19}
isLoading={isLoading}
onSelectChat={handleSelectChat}
onNewChat={() => setNewChatModalOpen(true)}
Expand Down
23 changes: 12 additions & 11 deletions client/components/page-components/chat/ChatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,48 @@

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Button } from "@/components/ui/button"
import { PlusCircle } from "lucide-react"
import { IChat } from "@/types"
import getRoleSpecificData from './getRoleSpecificData'
import { getSenderData } from './getSenderData'
import ChatListSkeleton from "@/components/skeletons/ChatList"
import { ButtonV2 } from "@/components/common/ButtonV2"

interface ChatListProps {
chats: IChat[];
onSelectChat: (id: string) => void;
onNewChat: () => void;
isDoctorData: boolean;
isLoading: boolean;
sender: "doctor" | "patient";
skeltonCount:number
}

export default function ChatList({ chats, onSelectChat, onNewChat, isDoctorData, isLoading }: ChatListProps) {
export default function ChatList({ chats, onSelectChat, onNewChat, isLoading, sender, skeltonCount }: ChatListProps) {
return (
<div className="flex flex-col h-full bg-background">
<div className="p-3 border-b space-y-3">
<Button className="w-full" variant="outline" size="sm" onClick={onNewChat}>
<ButtonV2 className="w-full" variant="ringHover" size="sm" onClick={onNewChat}>
<PlusCircle className="mr-2 h-4 w-4" />
New Chat
</Button>
</ButtonV2>
</div>
<ScrollArea className="flex-grow">
<div className="space-y-1 p-2">
{isLoading ? (
<ChatListSkeleton itemCount={10} />
<ChatListSkeleton itemCount={skeltonCount} />
) : (
chats.map(({ _id, doctorName, patientName, doctorProfile, patientProfile, notSeenMessages },i) => (
chats.map(({ _id, doctorName, patientName, doctorProfile, patientProfile, notSeenMessages }, i) => (
<div
key={`${_id},${i}`}
className="flex items-center space-x-2 p-2 rounded-lg transition-colors cursor-pointer hover:bg-accent/50 border border-border"
onClick={() => onSelectChat(_id!)}
>
<Avatar className="w-8 h-8 flex-shrink-0">
<AvatarImage src={getRoleSpecificData("patient", doctorProfile!, patientProfile!) || "/assets/icons/circle-user.svg"} alt={doctorName || patientName} />
<AvatarFallback>{(doctorName || patientName)?.charAt(0)}</AvatarFallback>
<AvatarImage src={getSenderData(sender, doctorProfile!, patientProfile!) || "/assets/icons/circle-user.svg"} alt={getSenderData(sender, doctorName!, patientName!)} />
<AvatarFallback>{getSenderData(sender, doctorName!, patientName!)?.charAt(0)}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="flex justify-between items-baseline">
<h3 className="font-medium text-sm truncate">{doctorName || patientName}</h3>
<h3 className="font-medium text-sm truncate">{getSenderData(sender, doctorName!, patientName!)}</h3>
{notSeenMessages! > 0 && (
<span className="bg-primary text-primary-foreground text-xs font-medium px-1.5 py-0.5 rounded-full">
{notSeenMessages}
Expand Down
27 changes: 12 additions & 15 deletions client/components/page-components/chat/ChatSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ArrowLeft, Send, AlertCircle } from "lucide-react"
import { IMessage } from "@/types"
import { IChat, IMessage } from "@/types"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Spinner } from "@/components/skeletons/spinner"
import { getSenderData } from "./getSenderData"

interface ChatSectionProps {
chatId: string;
sender:"doctor"|"patient";
messages: IMessage[];
onSendMessage: (message: string) => void;
isDoctor: boolean;
error?: string;
isError: boolean;
isPending:boolean;
isLoading: boolean;
chat:IChat;
error?: string;
}

const ChatSection = ({ chatId, messages, onSendMessage, isDoctor, error, isError, isLoading }: ChatSectionProps) => {
const ChatSection = ({ messages, onSendMessage, sender, error, isError, isLoading, chat, isPending }: ChatSectionProps) => {
const [message, setMessage] = useState("");
const router = useRouter()

Expand All @@ -31,11 +33,6 @@ const ChatSection = ({ chatId, messages, onSendMessage, isDoctor, error, isError
}
}

const onBack = () => {
router.back();
}


if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
Expand All @@ -60,15 +57,15 @@ const ChatSection = ({ chatId, messages, onSendMessage, isDoctor, error, isError
<div className="flex flex-col h-full bg-background">
<header className="p-4 border-b border-border flex-shrink-0">
<div className="flex items-center space-x-4">
<Button variant="ghost" size="icon" onClick={onBack} className="sm:hidden">
<Button variant="ghost" size="icon" onClick={()=>router.back()} className="sm:hidden">
<ArrowLeft className="h-6 w-6" />
<span className="sr-only">Back to chat list</span>
</Button>
<Avatar className="w-10 h-10">
<AvatarImage src="/assets/icons/circle-user.svg" alt={`User ${chatId}`} />
<AvatarFallback>{chatId}</AvatarFallback>
<AvatarImage src={getSenderData(sender,chat.doctorProfile!,chat.patientProfile!)||`/assets/icons/circle-user.svg`} alt={`${getSenderData(sender,chat.doctorName!,chat.patientName!)}`} />
<AvatarFallback>{sender}</AvatarFallback>
</Avatar>
<h2 className="text-xl font-semibold">Chat with User {chatId}</h2>
<h2 className="text-xl font-semibold">{getSenderData(sender,chat.doctorName!,chat.patientName!)}</h2>
</div>
</header>
<ScrollArea className="flex-grow p-4">
Expand All @@ -89,7 +86,7 @@ const ChatSection = ({ chatId, messages, onSendMessage, isDoctor, error, isError
<div className="flex items-center gap-2">
<Input
className="flex-1"
placeholder={`Message user ${chatId}`}
placeholder={`Message user ${getSenderData(sender,chat.doctorName!,chat.patientName!)}`}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
Expand Down
15 changes: 0 additions & 15 deletions client/components/page-components/chat/getRoleSpecificData.ts

This file was deleted.

14 changes: 14 additions & 0 deletions client/components/page-components/chat/getSenderData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const getSenderData = (sender: "doctor" | "patient", doctorData: string, patientData: string):string => {
if (sender === 'patient') {
return patientData
} else {
return doctorData ;
}
}


export const getSenderSpecificList = <T>(isDoctor: boolean, doctorData: T[] = [], patientData: T[] = []): T[] => {
return isDoctor ? doctorData : patientData;
};


6 changes: 3 additions & 3 deletions client/lib/hooks/chat/useChatDoctor.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { getDoctorChats, getMessagesOfDoctorChat, createChatDoctor, createMessageDoctor } from "@/lib/api/chat";
import { ErrorResponse, MessageResponse } from "@/types";
import { ErrorResponse, IChat, IMessage, MessageResponse, PaginatedResult } from "@/types";
import { useMutation, useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";

export const useGetDoctorChats = () => {
return useQuery({
return useQuery<IChat[],AxiosError<ErrorResponse>>({
queryKey: ['doctor-chat-list'],
queryFn: () => getDoctorChats()
});
}

export const useGetDoctorMessages = (chatId: string, limit: number) => {
return useQuery({
return useQuery<{ chat: IChat, data: PaginatedResult<IMessage> }>({
queryKey: ['doctor-messages', chatId],
queryFn: () => getMessagesOfDoctorChat(chatId, limit)
});
Expand Down
8 changes: 5 additions & 3 deletions client/lib/hooks/chat/useChatPatient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getMessagesOfPatientChat, getPatientChats, createChatPatient, createMessagePatient, } from "@/lib/api/chat";
import { ErrorResponse, IMessage, MessageResponse, PaginatedResult } from "@/types";
import { ErrorResponse, IChat, IMessage, MessageResponse, PaginatedResult } from "@/types";
import { useMutation, useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";

Expand All @@ -11,9 +11,11 @@ export const useGetPatientChats = () => {
}

export const useGetPatientMessages = (chatId: string, limit: number) => {
return useQuery<PaginatedResult<IMessage>,AxiosError<ErrorResponse>>({
return useQuery< { chat:IChat, data:PaginatedResult<IMessage> },AxiosError<ErrorResponse>>({
queryKey: ['messages', chatId],
queryFn: () => getMessagesOfPatientChat(chatId, limit)
queryFn: () => getMessagesOfPatientChat(chatId, limit),
refetchOnMount:true,
refetchInterval:1000
})
}

Expand Down
4 changes: 2 additions & 2 deletions server/src/presentation/controllers/chat/ChatControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export default class ChatController {
const chatId = req.params.chatId;
let limit = +(req.query.limit as string);
limit = isNaN(limit) || limit < 0 ? 10 : Math.min(limit, 100);
const messages = await this.getChatUseCase.getMessagesOfChat(chatId, limit);
res.status(StatusCode.Success).json(messages);
const { chat, data } = await this.getChatUseCase.getMessagesOfChat(chatId, limit);
res.status(StatusCode.Success).json({ chat, data });
} catch (error) {
next(error)
}
Expand Down
1 change: 0 additions & 1 deletion server/src/use_case/chat/CreateChatUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export default class CreateChatUseCase {
async createMessage(chatId: string, receiverId: string, message: string, senderId: string): Promise<void> {
this.validatorService.validateRequiredFields({ chatId, receiverId, message, senderId });
this.validatorService.validateMultipleIds([chatId, receiverId, senderId]);
this.validatorService.validateLength(message, 1);
await this.messageRepository.create({ chatId, message, receiverId, senderId, isReceived: false });
}
}
16 changes: 10 additions & 6 deletions server/src/use_case/chat/GetChatUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import CustomError from "../../domain/entities/CustomError";
import IChat, { IChatWithNotSeenCount } from "../../domain/entities/IChat";
import IMessage from "../../domain/entities/IMessage";
import IChatRepository from "../../domain/interface/repositories/IChatRepository";
import IMessageRepository from "../../domain/interface/repositories/IMessageRepository";
import IValidatorService from "../../domain/interface/services/IValidatorService";
import { PaginatedResult } from "../../types";
import { PaginatedResult, StatusCode } from "../../types";

export default class GetChatUseCase {
constructor(
Expand All @@ -24,21 +25,24 @@ export default class GetChatUseCase {
return await this.getChatsWithNotSeenMessages(doctorId, chats);
}

async getMessagesOfChat(chatId: string, limit: number): Promise<PaginatedResult<IMessage>> {
async getMessagesOfChat(chatId: string, limit: number): Promise<{ data: PaginatedResult<IMessage>, chat: IChat }> {
this.validatorService.validateIdFormat(chatId);
const offset = 0;
return await this.messageRepository.findByChatId(chatId, limit, offset);
const chat = await this.chatRepository.findById(chatId);
if (!chat) throw new CustomError("Not found", StatusCode.NotFound);
const data = await this.messageRepository.findByChatId(chatId, limit, offset);
return { data, chat }
}

private async getChatsWithNotSeenMessages(
receiverId: string,
chats: IChat[]
): Promise<IChatWithNotSeenCount[] | []> {
const unreadMessages = await this.messageRepository.getUnreadMessageCountGroupedByChat(receiverId);
if(!unreadMessages){
return chats.map(el=>({...el,notSeenMessages:0}));
if (!unreadMessages) {
return chats.map(el => ({ ...el, notSeenMessages: 0 }));
}
return chats
}

}

1 comment on commit 78e7a38

@vercel
Copy link

@vercel vercel bot commented on 78e7a38 Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.