Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/swagger.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"title": "Moji",
"title": "Chatify",
"version": "1.0.0",
"description": ""
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/auth/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const ProtectedRoute = () => {
<LoaderIcon
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin")}
className="size-4 animate-spin dark:text-white"
/>
</div>
);
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/chat/layout/ChatWelcomeScreeen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SidebarInset } from "@/components/ui/sidebar";
import ChatWindowHeader from "./component/ChatWindowHeader";

const ChatWelcomeScreen = () => {
return (
<SidebarInset className="flex w-full h-full bg-transparent">
<ChatWindowHeader />
<div className="flex bg-primary-foreground rounded-2xl flex-1 items-center justify-center">
<div className="text-center">
<div className="size-24 mx-auto mb-6 bg-gradient-chat rounded-full flex items-center justify-center shadow-glow pulse-ring">
<span className="text-3xl">💬</span>
</div>
<h2 className="text-2xl font-bold mb-2 bg-gradient-chat bg-clip-text text-transparent">
Chào mừng bạn đến với Chatify!
</h2>
<p className="text-muted-foreground">
Chọn một cuộc hội thoại để bắt đầu chat!
</p>
</div>
</div>
</SidebarInset>
);
};

export default ChatWelcomeScreen;
32 changes: 31 additions & 1 deletion frontend/src/components/chat/layout/ChatWindowLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
import { useChatStore } from "@/stores/useChatStore";
import ChatWelcomeScreeen from "./ChatWelcomeScreeen";
import ChatWindowSkeleton from "./ChatWindowSkeleton";
import { SidebarInset } from "@/components/ui/sidebar";
import ChatWindowHeader from "./component/ChatWindowHeader";
import ChatWindowBody from "./component/ChatWindowBody";
import MessageInput from "./component/MessageInput";

const ChatWindowLayout = () => {
const { activeConversationId, conversations, messageLoading: loading, messages } = useChatStore();
const selectedConvo = conversations.find((c) => c._id === activeConversationId) ?? null

if (!selectedConvo) {
return <ChatWelcomeScreeen />
}

if (loading) {
return <ChatWindowSkeleton />
}
return (
<h2>ChatWindowLayout</h2>
<SidebarInset className="flex flex-col h-full flex-1 overflow-hidden rounded-sm shadow-md">

{/* Header */}
<ChatWindowHeader />

{/* Body */}
<div className="flex-1 overflow-y-auto bg-primary-foreground">
<ChatWindowBody/>
</div>

{/* Footer */}
<MessageInput/>
</SidebarInset>
)
};

Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/chat/layout/ChatWindowSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

const ChatWindowSkeleton = () => {
return (
<div>ChatWindowSkeleton</div>
)
}

export default ChatWindowSkeleton
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

const ChatWindowBody = () => {
return (
<div>ChatWindowBody</div>
)
}

export default ChatWindowBody
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

const ChatWindowHeader = () => {
return (
<div>ChatWindowHeader</div>
)
}

export default ChatWindowHeader
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

const MessageInput = () => {
return (
<div>MessageInput</div>
)
}

export default MessageInput
4 changes: 2 additions & 2 deletions frontend/src/components/chat/sidebar/DirectMessageCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import UnreadCountBadge from "../shared/UnreadCountBadge";

const DirectMessageCard = ({convo} : {convo:Conversation}) => {
const {user} = useAuthStore();
const {activeConversationId , setActiveConversation , messages} = useChatStore();
const {activeConversationId , setActiveConversation , messages, fetchMessage} = useChatStore();

if(!user) return null;

Expand All @@ -22,7 +22,7 @@ const DirectMessageCard = ({convo} : {convo:Conversation}) => {
const handleSelectConversation = async (id:string) => {
setActiveConversation(id);
if(!messages[id]){
//fetch message
await fetchMessage();
}
}
return (
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/chat/sidebar/GropuMessageCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import GroupChatAvatar from './GroupChatAvatar';

const GropuMessageCard = ({ convo }: { convo: Conversation }) => {
const { user } = useAuthStore();
const { activeConversationId, setActiveConversation, messages } = useChatStore();
const { activeConversationId, setActiveConversation, messages , fetchMessage} = useChatStore();

if (!user) return null;

Expand All @@ -17,7 +17,7 @@ const GropuMessageCard = ({ convo }: { convo: Conversation }) => {
const handleSelectConversation = async (id: string) => {
setActiveConversation(id);
if (!messages[id]) {
//fetch message
fetchMessage();
}
}
return (
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/pages/ChatAppPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ const ChatAppPage = () => {

return (
<SidebarProvider>
<AppSidebar>
<div className="flex h-screen w-full p-2">
<ChatWindowLayout/>
</div>
</AppSidebar>
<AppSidebar />
<div className="flex h-screen w-full p-2">
<ChatWindowLayout />
</div>
</SidebarProvider>
);

Expand Down
12 changes: 12 additions & 0 deletions frontend/src/services/chatService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import api from "@/lib/axios";
import type { ConversationResponse, Message } from "@/types/chat";

interface FetchMessageProps {
messages: Message[];
cursor?:string
}

const pageLimit = 50;

export const chatService = {
async fetchConversations() : Promise<ConversationResponse>{
const res = await api.get("/conversations");
return res.data;
},

async fetchMessage(id:string, cursor:string) : Promise<FetchMessageProps> {
const res = await api.get(`/conversation/${id}/message?limit=${pageLimit}&cursor=${cursor}`)
return {messages: res.data.m.messages, cursor: res.data.nextCursor}
}
}
62 changes: 55 additions & 7 deletions frontend/src/stores/useChatStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { chatService } from "@/services/chatService"
import type { ChatState } from "@/types/store"
import { create } from "zustand"
import { persist } from "zustand/middleware"
import { useAuthStore } from "./useAuthStore"

export const useChatStore = create<ChatState>()(
persist(
(set, get) => ({
conversations: [],
messages: {},
activeConversationId: null,
loading: false,
convoLoading: false,
messageLoading: false,

setActiveConversation: (id) => set({ activeConversationId: id }),

Expand All @@ -18,18 +20,64 @@ export const useChatStore = create<ChatState>()(
conversations: [],
messages: {},
activeConversationId: null,
loading: false,
convoLoading: false,
messageLoading: false
})
},

fetchConversatons: async () => {
try {
set({loading:true});
const {conversations} = await chatService.fetchConversations();
set({conversations,loading:false})
set({ convoLoading: true });
const { conversations } = await chatService.fetchConversations();
set({ conversations, convoLoading: false })
} catch (error) {
console.error("Lỗi xảy ra khi fetchConversation:",error)
set({loading:false})
console.error("Lỗi xảy ra khi fetchConversation:", error)
set({ convoLoading: false })
}
},

fetchMessage: async (conversationId) => {
const { activeConversationId, messages } = get();
const { user } = useAuthStore.getState();

const convoId = conversationId ?? activeConversationId;

if (!convoId) return;

const current = messages?.[convoId];
const nextCursor = current?.nextCursor === undefined ? "" : current?.nextCursor;

if (nextCursor === null) return

set({ messageLoading: true })

try {
const { messages: fetched, cursor } = await chatService.fetchMessage(convoId, nextCursor);
const processed = fetched.map((m) => ({
...m,
isOwn:m.senderId === user?._id
}))

set( (state) => {
const prev = state.messages[convoId]?.items ?? [];
const merged = prev.length > 0 ? [...processed, ...prev] : processed;

return {
messages:{
...state.messages,
[convoId] : {
items:merged,
hasMore:!!cursor,
nextCursor:cursor ?? null,
}
}
}
})

} catch (error) {
console.error("Loi xay ra khi fetch Message:",error);
} finally{
set({messageLoading:false})
}
}
}), {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/types/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ export interface ChatState{
nextCursor?: string | null
}>;
activeConversationId: string | null;
loading:boolean;
convoLoading:boolean;
messageLoading:boolean;
reset: () => void;
setActiveConversation: (id:string | null) => void;
fetchConversatons: () => Promise<void>
fetchConversatons: () => Promise<void>;
fetchMessage:(conversationId?:string) => Promise<void>
}