diff --git a/backend/src/swagger.json b/backend/src/swagger.json index 6d4608b..bee23f0 100644 --- a/backend/src/swagger.json +++ b/backend/src/swagger.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "title": "Moji", + "title": "Chatify", "version": "1.0.0", "description": "" }, diff --git a/frontend/src/components/auth/ProtectedRoute.tsx b/frontend/src/components/auth/ProtectedRoute.tsx index 19e28ee..1af6506 100644 --- a/frontend/src/components/auth/ProtectedRoute.tsx +++ b/frontend/src/components/auth/ProtectedRoute.tsx @@ -41,7 +41,7 @@ const ProtectedRoute = () => { ); diff --git a/frontend/src/components/chat/layout/ChatWelcomeScreeen.tsx b/frontend/src/components/chat/layout/ChatWelcomeScreeen.tsx new file mode 100644 index 0000000..12df44a --- /dev/null +++ b/frontend/src/components/chat/layout/ChatWelcomeScreeen.tsx @@ -0,0 +1,25 @@ +import { SidebarInset } from "@/components/ui/sidebar"; +import ChatWindowHeader from "./component/ChatWindowHeader"; + +const ChatWelcomeScreen = () => { + return ( + + +
+
+
+ 💬 +
+

+ Chào mừng bạn đến với Chatify! +

+

+ Chọn một cuộc hội thoại để bắt đầu chat! +

+
+
+
+ ); +}; + +export default ChatWelcomeScreen; \ No newline at end of file diff --git a/frontend/src/components/chat/layout/ChatWindowLayout.tsx b/frontend/src/components/chat/layout/ChatWindowLayout.tsx index 29d3950..196dbbb 100644 --- a/frontend/src/components/chat/layout/ChatWindowLayout.tsx +++ b/frontend/src/components/chat/layout/ChatWindowLayout.tsx @@ -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 + } + + if (loading) { + return + } return ( -

ChatWindowLayout

+ + + {/* Header */} + + + {/* Body */} +
+ +
+ + {/* Footer */} + +
) }; diff --git a/frontend/src/components/chat/layout/ChatWindowSkeleton.tsx b/frontend/src/components/chat/layout/ChatWindowSkeleton.tsx new file mode 100644 index 0000000..6757db0 --- /dev/null +++ b/frontend/src/components/chat/layout/ChatWindowSkeleton.tsx @@ -0,0 +1,8 @@ + +const ChatWindowSkeleton = () => { + return ( +
ChatWindowSkeleton
+ ) +} + +export default ChatWindowSkeleton \ No newline at end of file diff --git a/frontend/src/components/chat/layout/component/ChatWindowBody.tsx b/frontend/src/components/chat/layout/component/ChatWindowBody.tsx new file mode 100644 index 0000000..9203412 --- /dev/null +++ b/frontend/src/components/chat/layout/component/ChatWindowBody.tsx @@ -0,0 +1,8 @@ + +const ChatWindowBody = () => { + return ( +
ChatWindowBody
+ ) +} + +export default ChatWindowBody \ No newline at end of file diff --git a/frontend/src/components/chat/layout/component/ChatWindowHeader.tsx b/frontend/src/components/chat/layout/component/ChatWindowHeader.tsx new file mode 100644 index 0000000..2aa6fa4 --- /dev/null +++ b/frontend/src/components/chat/layout/component/ChatWindowHeader.tsx @@ -0,0 +1,8 @@ + +const ChatWindowHeader = () => { + return ( +
ChatWindowHeader
+ ) +} + +export default ChatWindowHeader \ No newline at end of file diff --git a/frontend/src/components/chat/layout/component/MessageInput.tsx b/frontend/src/components/chat/layout/component/MessageInput.tsx new file mode 100644 index 0000000..dc75881 --- /dev/null +++ b/frontend/src/components/chat/layout/component/MessageInput.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const MessageInput = () => { + return ( +
MessageInput
+ ) +} + +export default MessageInput \ No newline at end of file diff --git a/frontend/src/components/chat/sidebar/DirectMessageCard.tsx b/frontend/src/components/chat/sidebar/DirectMessageCard.tsx index 8087ad9..f81fdf6 100644 --- a/frontend/src/components/chat/sidebar/DirectMessageCard.tsx +++ b/frontend/src/components/chat/sidebar/DirectMessageCard.tsx @@ -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; @@ -22,7 +22,7 @@ const DirectMessageCard = ({convo} : {convo:Conversation}) => { const handleSelectConversation = async (id:string) => { setActiveConversation(id); if(!messages[id]){ - //fetch message + await fetchMessage(); } } return ( diff --git a/frontend/src/components/chat/sidebar/GropuMessageCard.tsx b/frontend/src/components/chat/sidebar/GropuMessageCard.tsx index ca4f5fd..17a0fd2 100644 --- a/frontend/src/components/chat/sidebar/GropuMessageCard.tsx +++ b/frontend/src/components/chat/sidebar/GropuMessageCard.tsx @@ -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; @@ -17,7 +17,7 @@ const GropuMessageCard = ({ convo }: { convo: Conversation }) => { const handleSelectConversation = async (id: string) => { setActiveConversation(id); if (!messages[id]) { - //fetch message + fetchMessage(); } } return ( diff --git a/frontend/src/pages/ChatAppPage.tsx b/frontend/src/pages/ChatAppPage.tsx index 5f90161..7c39076 100644 --- a/frontend/src/pages/ChatAppPage.tsx +++ b/frontend/src/pages/ChatAppPage.tsx @@ -5,11 +5,10 @@ const ChatAppPage = () => { return ( - -
- -
-
+ +
+ +
); diff --git a/frontend/src/services/chatService.ts b/frontend/src/services/chatService.ts index 8ab1569..d172157 100644 --- a/frontend/src/services/chatService.ts +++ b/frontend/src/services/chatService.ts @@ -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{ const res = await api.get("/conversations"); return res.data; + }, + + async fetchMessage(id:string, cursor:string) : Promise { + const res = await api.get(`/conversation/${id}/message?limit=${pageLimit}&cursor=${cursor}`) + return {messages: res.data.m.messages, cursor: res.data.nextCursor} } } \ No newline at end of file diff --git a/frontend/src/stores/useChatStore.tsx b/frontend/src/stores/useChatStore.tsx index 051c493..3f3a1da 100644 --- a/frontend/src/stores/useChatStore.tsx +++ b/frontend/src/stores/useChatStore.tsx @@ -2,6 +2,7 @@ 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()( persist( @@ -9,7 +10,8 @@ export const useChatStore = create()( conversations: [], messages: {}, activeConversationId: null, - loading: false, + convoLoading: false, + messageLoading: false, setActiveConversation: (id) => set({ activeConversationId: id }), @@ -18,18 +20,64 @@ export const useChatStore = create()( 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}) } } }), { diff --git a/frontend/src/types/store.ts b/frontend/src/types/store.ts index e26e256..9bcee1d 100644 --- a/frontend/src/types/store.ts +++ b/frontend/src/types/store.ts @@ -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 + fetchConversatons: () => Promise; + fetchMessage:(conversationId?:string) => Promise } \ No newline at end of file