diff --git a/src/service/feature/channel/hook/query/useChannelQuery.ts b/src/service/feature/channel/hook/query/useChannelQuery.ts index 2a3299f..3b99034 100644 --- a/src/service/feature/channel/hook/query/useChannelQuery.ts +++ b/src/service/feature/channel/hook/query/useChannelQuery.ts @@ -1,13 +1,9 @@ import { useQuery } from '@tanstack/react-query'; -import { - getChannelList, - getDMList, -} from '@service/feature/channel/api/channelAPI.ts'; -import { Channel } from '@service/feature/channel/types/channel.ts'; +import { getChannelList, getDMList } from '@service/feature/channel/api/channelAPI.ts'; +import {ChannelResponse} from '@service/feature/channel/types/channel.ts'; -// response 형식이 다름. 임시로 설정하여 사용중 export const useChannelListQuery = (serverId: string) => { - return useQuery({ + return useQuery({ queryKey: ['serverChannels', serverId], queryFn: () => getChannelList(serverId), enabled: !!serverId, diff --git a/src/service/feature/channel/types/channel.ts b/src/service/feature/channel/types/channel.ts index 785f346..ad8492e 100644 --- a/src/service/feature/channel/types/channel.ts +++ b/src/service/feature/channel/types/channel.ts @@ -1,19 +1,11 @@ export type ChannelType = 'text' | 'voice' | 'event'; -// export interface Channel { -// id: string; -// name: string; -// type: ChannelType; -// category: string; -// [key: string]: unknown; -// } - export interface DMDetail { - channel: Channel2; + channel: Channel; channelMembers: ChannelMember[]; } -export interface DMList extends Channel2 { +export interface DMList extends Channel { channelMembers: ChannelMember[]; } @@ -26,8 +18,7 @@ export interface ChannelMember { createdAt: string; } -// 팀 서버 상세 조회에서 불러오는 channel 타입도 이것. 추후 아래 Channel 타입에서 이걸로 변경해야 할 듯 -export interface Channel2 { +export interface Channel { id: number; name: string; position: number; @@ -36,29 +27,36 @@ export interface Channel2 { chatId: string; } -export interface Channel { - categoriesView: CategoriesView[]; - team: Team; - teamMembers: TeamMembers[]; -} - -export interface CategoriesView { +export interface CategoryView { category: { id: number; name: string; position: number; }; -} + channels: Channel[]; -export interface Team { - id: string; - name: string; - masterId: string; - iconUrl: string; } -export interface TeamMembers { - id: number; - role: 'OWNER' | 'MEMBER'; - memberInfo: ChannelMember; +export interface ChannelResponse { + team: { + id: string; + name: string; + masterId: string; + iconUrl: string; + }; + categoriesView: CategoryView[]; + teamMembers: { + id: number; + role: 'OWNER' | 'MEMBER'; + memberInfo: ChannelMember; + }[]; } + +export interface ChannelMember { + id: string; + nickname: string; + name: string; + avatarUrl: string; + state: 'ONLINE' | 'OFFLINE'; + createdAt: string; +} \ No newline at end of file diff --git a/src/service/feature/chat/api/chatAPI.ts b/src/service/feature/chat/api/chatAPI.ts index 924b676..03f7f61 100644 --- a/src/service/feature/chat/api/chatAPI.ts +++ b/src/service/feature/chat/api/chatAPI.ts @@ -7,9 +7,9 @@ export const fetchChannels = async () => { return res.data; }; -export const fetchMessages = async (channelId: string) => { - const res = await axios.get(`/channels/${channelId}/messages`); - return res.data; +export const fetchLatestMessages = async (channelId: string | undefined) => { + const res = await axios.get(`/message/latest?chatId=${channelId}`); + return Array.isArray(res.data) ? res.data : []; }; export const deleteMessage = async (messageId: string) => { diff --git a/src/service/feature/chat/hook/useChat.ts b/src/service/feature/chat/hook/useChat.ts index 96d819d..ddc0c0c 100644 --- a/src/service/feature/chat/hook/useChat.ts +++ b/src/service/feature/chat/hook/useChat.ts @@ -1,36 +1,82 @@ -import { useEffect } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { useEffect, useCallback } from 'react'; import { useSocket } from '../context/useSocket'; import { ChatMessage } from '@service/feature/chat/schema/messageSchema.ts'; -export const useChat = (onMessage: (msg: ChatMessage) => void) => { +export const useChat = (chatId: string | undefined, onMessage: (msg: ChatMessage) => void) => { const { client, isConnected } = useSocket(); - const chatId = '25ffc7bf-874f-444e-b331-26ed864a76ba'; useEffect(() => { - if (!client || !isConnected) return; + let subscription: any; + + const setupSubscription = () => { + if (!client || !isConnected || !chatId) { + console.log('채팅 구독 조건이 충족되지 않음:', { client: !!client, isConnected, chatId }); + return; + } + + try { + const subscribeUrl = `/sub/message/${chatId}`; + console.log('채팅 구독 시도:', subscribeUrl); + + subscription = client.subscribe(subscribeUrl, (message) => { + const parsed: ChatMessage = JSON.parse(message.body); + onMessage(parsed); + }); + + console.log('채팅 구독 성공'); + } catch (error) { + console.error('STOMP 구독 중 오류 발생:', error); + } + }; - const subscribeUrl = `/sub/message/${chatId}`; - const subscription = client.subscribe(subscribeUrl, (message) => { - const parsed: ChatMessage = JSON.parse(message.body); - onMessage(parsed); - }); + setupSubscription(); return () => { - subscription.unsubscribe(); + if (subscription) { + try { + subscription.unsubscribe(); + console.log('채팅 구독 해제'); + } catch (error) { + console.error('구독 해제 중 오류 발생:', error); + } + } }; - }, [client, isConnected, onMessage]); + }, [client, isConnected, chatId, onMessage]); - const sendMessage = (content: string, attachments?: { type: string; url: string }[]) => { - if (!client || !isConnected) return; + const sendMessage = useCallback(async (content: string, attachments?: { type: string; url: string }[]) => { + if (!client || !isConnected || !chatId) { + console.warn('메시지를 보낼 수 없습니다:', { + client: !!client, + isConnected, + chatId + }); + return Promise.reject(new Error('연결 상태가 올바르지 않습니다.')); + } + const tempId = uuidv4(); const sendUrl = `/pub/message/${chatId}`; - const message = { chatId, content, attachments, createdAt: new Date().toISOString()}; + const message = { + chatId, + content, + attachments, + createdAt: new Date().toISOString(), + tempId + }; - client.publish({ - destination: sendUrl, - body: JSON.stringify(message), + return new Promise((resolve, reject) => { + try { + client.publish({ + destination: sendUrl, + body: JSON.stringify(message), + }); + resolve(tempId); + } catch (error) { + console.error('메시지 전송 중 오류 발생:', error); + reject(error); + } }); - }; + }, [client, isConnected, chatId]); - return { sendMessage }; + return { sendMessage, isConnected }; }; \ No newline at end of file diff --git a/src/service/feature/chat/hook/useMessageHistory.ts b/src/service/feature/chat/hook/useMessageHistory.ts index 0845ea7..e804dc6 100644 --- a/src/service/feature/chat/hook/useMessageHistory.ts +++ b/src/service/feature/chat/hook/useMessageHistory.ts @@ -1,10 +1,11 @@ import { useQuery } from '@tanstack/react-query'; -import { fetchMessages } from '../api/chatAPI'; +import { fetchLatestMessages } from '../api/chatAPI'; -export const useMessageHistory = (channelId: string) => { +export const useMessageHistory = (channelId: string | undefined) => { return useQuery({ queryKey: ['messages', channelId], - queryFn: () => fetchMessages(channelId), + queryFn: () => fetchLatestMessages(channelId), enabled: !!channelId, + staleTime: 1000*30 }); }; \ No newline at end of file diff --git a/src/service/feature/chat/schema/messageSchema.ts b/src/service/feature/chat/schema/messageSchema.ts index a9f7481..9a3e2f4 100644 --- a/src/service/feature/chat/schema/messageSchema.ts +++ b/src/service/feature/chat/schema/messageSchema.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; const senderSchema = z.object({ memberId: z.string(), - username: z.string(), + name: z.string(), avatarUrl: z.string(), }); @@ -12,13 +12,15 @@ const attachmentSchema = z.object({ }); export const messageSchema = z.object({ - chatId: z.string(), + messageId: z.number(), sender: senderSchema, content: z.string().min(1, '메시지를 입력해주세요'), + createdAt: z.string().datetime({ message: '올바른 날짜/시간 형식이 아닙니다' }), + isUpdated: z.boolean(), + isDeleted: z.boolean(), attachments: z.array(attachmentSchema).optional(), - createdAt: z - .string() - .datetime({ message: '올바른 날짜/시간 형식이 아닙니다' }), + status: z.enum(['pending', 'sent', 'error']).optional(), + tempId: z.string().optional(), }); export type ChatMessage = z.infer; \ No newline at end of file diff --git a/src/service/feature/chat/type/message.ts b/src/service/feature/chat/type/message.ts new file mode 100644 index 0000000..4dbd025 --- /dev/null +++ b/src/service/feature/chat/type/message.ts @@ -0,0 +1,28 @@ +// "messageId": 13, +// "sender": { +// "memberId": "fc810ff3-a156-410c-80db-939440507dc3", +// "name": "최승은", +// "avatarUrl": "" +// }, +// "content": "ccc", +// "createdAt": "2025-06-03T22:34:07.542118", +// "isUpdated": false, +// "isDeleted": false, +// "attachments": [] +// }, + +export interface ChatMessage { + messageId : number; + sender: { + memberId: string; + name: string; + avatarUrl: string; + }, + content: string, + createdAt: string, + isUpdated: boolean, + isDeleted: boolean, + attachments?: { type: string; url: string }[]; + status?: 'pending' | 'sent' | 'error'; + tempId?: string; +} \ No newline at end of file diff --git a/src/view/layout/sidebar/components/channel/ChannelCategory.tsx b/src/view/layout/sidebar/components/channel/ChannelCategory.tsx index 4e8283b..2f96c2d 100644 --- a/src/view/layout/sidebar/components/channel/ChannelCategory.tsx +++ b/src/view/layout/sidebar/components/channel/ChannelCategory.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { DndContext } from '@dnd-kit/core'; +import {DndContext, DragEndEvent} from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy, @@ -9,11 +9,7 @@ import { ChevronDown, ChevronRight, Plus } from 'lucide-react'; import ChannelItem from './ChannelItem.tsx'; import { Channel } from '@service/feature/channel/types/channel.ts'; -const ChannelCategory = ({ - title, - type, - defaultItems, - }: { +const ChannelCategory = ({title, type, defaultItems,}: { title: string; type: 'text' | 'voice' | 'event'; defaultItems: Channel[]; @@ -21,9 +17,9 @@ const ChannelCategory = ({ const [isOpen, setIsOpen] = useState(true); const [items, setItems] = useState(defaultItems); - const handleDragEnd = (event: any) => { + const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; - if (active.id !== over?.id) { + if (over && active.id !== over.id) { const oldIndex = items.findIndex((i) => i.id === active.id); const newIndex = items.findIndex((i) => i.id === over.id); setItems((items) => arrayMove(items, oldIndex, newIndex)); @@ -53,6 +49,7 @@ const ChannelCategory = ({ id={item.id} name={item.name} type={type} + chatId={item.chatId} /> ))} diff --git a/src/view/layout/sidebar/components/channel/ChannelItem.tsx b/src/view/layout/sidebar/components/channel/ChannelItem.tsx index 92cf477..fcac574 100644 --- a/src/view/layout/sidebar/components/channel/ChannelItem.tsx +++ b/src/view/layout/sidebar/components/channel/ChannelItem.tsx @@ -1,31 +1,41 @@ -import { useDraggable } from '@dnd-kit/core'; -import { Hash, Radio, Volume2 } from 'lucide-react'; -import { clsx } from 'clsx'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { Hash } from 'lucide-react'; -const ChannelItem = ({ id, name, type = 'text', selected = false, }: { - id: string; +interface ChannelItemProps { + id: number; name: string; type?: 'text' | 'voice' | 'event'; - selected?: boolean; -}) => { - const icon = - type === 'voice' ? : - type === 'event' ? : - ; + chatId: string; +} - const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id, }); +const ChannelItem = ({ id, name, chatId }: ChannelItemProps) => { + const navigate = useNavigate(); + const { serverId } = useParams<{ serverId: string }>(); + + const {attributes, listeners, setNodeRef, transform, transition,} = useSortable({ id }); - const style = transform - ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, opacity: 0.8, } - : undefined; + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const handleClick = () => { + navigate(`/channels/${serverId}/${chatId}`); + }; return ( -
- {icon} - {name} +
+
+ + {name} +
); }; diff --git a/src/view/layout/sidebar/components/channel/InviteFriendModal.tsx b/src/view/layout/sidebar/components/channel/InviteFriendModal.tsx index 73c80f2..b07af92 100644 --- a/src/view/layout/sidebar/components/channel/InviteFriendModal.tsx +++ b/src/view/layout/sidebar/components/channel/InviteFriendModal.tsx @@ -1,4 +1,5 @@ import Item from './Item'; +import { v4 as uuidv4 } from 'uuid'; import SearchFriends from '@pages/Friends/components/SearchFriends'; import { useEffect, useMemo, useState } from 'react'; import Modal from '@components/common/Modal'; @@ -45,7 +46,9 @@ const InviteFriendModal = ({ return ( - + @@ -62,7 +65,7 @@ const InviteFriendModal = ({
{searchData?.map((member) => ( - + ))}
diff --git a/src/view/layout/sidebar/components/channel/ServerChannelList.tsx b/src/view/layout/sidebar/components/channel/ServerChannelList.tsx index 22d07ee..8f88e1a 100644 --- a/src/view/layout/sidebar/components/channel/ServerChannelList.tsx +++ b/src/view/layout/sidebar/components/channel/ServerChannelList.tsx @@ -1,38 +1,42 @@ -import { useParams } from 'react-router-dom'; -import ChannelCategory from './ChannelCategory.tsx'; +import { useParams, useNavigate, useLocation } from 'react-router-dom'; +import { useEffect } from 'react'; import { useChannelListQuery } from '@service/feature/channel/hook/query/useChannelQuery.ts'; -import { Channel } from '@service/feature/channel/types/channel.ts'; import InviteFriendModal from './InviteFriendModal.tsx'; +import ChannelCategory from "./ChannelCategory.tsx"; const ServerChannelList = () => { - const { serverId } = useParams<{ serverId: string }>(); + const { serverId, channelId } = useParams<{ serverId: string; channelId: string }>(); + const navigate = useNavigate(); +// Line 10 removed const { data: channels, isLoading, error } = useChannelListQuery(serverId!); - console.log('channels: ', channels); + useEffect(() => { + if (channels?.categoriesView && channels.categoriesView.length > 0 && !channelId) { + const firstCategory = channels.categoriesView[0]; + if (firstCategory.channels && firstCategory.channels.length > 0) { + const firstChannel = firstCategory.channels[0]; + navigate(`/channels/${serverId}/${firstChannel.chatId}`, { replace: true }); + } + } + }, [channels, serverId, channelId, navigate]); if (isLoading) return
Loading...
; if (error) return
에러 발생
; - - // const categories = - // channels?.reduce((acc: Record, channel: Channel) => { - // if (!acc[channel.category]) acc[channel.category] = []; - // acc[channel.category].push(channel); - // return acc; - // }, {}) ?? {}; + if (!channels?.categoriesView) return null; return ( -
- {/* {Object.entries(categories).map(([categoryName, categoryChannels]) => ( +
+ {channels.categoriesView.map((categoryView) => ( - ))} */} - + ))} +
); }; -export default ServerChannelList; +export default ServerChannelList; \ No newline at end of file diff --git a/src/view/layout/sidebar/components/channel/SidebarLayout.tsx b/src/view/layout/sidebar/components/channel/SidebarLayout.tsx index 07a1536..db6a77b 100644 --- a/src/view/layout/sidebar/components/channel/SidebarLayout.tsx +++ b/src/view/layout/sidebar/components/channel/SidebarLayout.tsx @@ -1,6 +1,6 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => ( -
-
+
+
{children}
diff --git a/src/view/pages/chat/ChatPage.tsx b/src/view/pages/chat/ChatPage.tsx index 68715f3..8181c2a 100644 --- a/src/view/pages/chat/ChatPage.tsx +++ b/src/view/pages/chat/ChatPage.tsx @@ -1,67 +1,126 @@ -import { useState } from 'react'; -import { useChat } from '@service/feature/chat/hook/useChat.ts'; -import { ChatMessage } from '@service/feature/chat/schema/messageSchema.ts'; -import { ChannelHeader } from './components/layout/ChannelHeader'; -import { ChatInput } from '@pages/chat/components/layout/ChatInput.tsx'; -import { ChatView } from '@pages/chat/components/layout/ChatView.tsx'; +import { useMessageHistory } from "@service/feature/chat"; +import { useChat } from "@service/feature/chat/hook/useChat.ts"; +import { ChatMessage } from "@service/feature/chat/schema/messageSchema.ts"; +import { useState, useCallback, useEffect } from "react"; +import { ChannelHeader } from "./components/layout/ChannelHeader"; +import { ChatInput } from "@pages/chat/components/layout/ChatInput.tsx"; +import { ChatView } from "@pages/chat/components/layout/ChatView.tsx"; +import { postImage } from "@service/feature/image/imageApi.ts"; +import { useParams } from "react-router-dom"; +import {v4 as uuidv4} from "uuid"; -const CHAT_ID = '25ffc7bf-874f-444e-b331-26ed864a76ba'; -const MY_ID = 'tester'; +const MY_ID = "tests"; export function ChatPage() { - const [messages, setMessages] = useState([]); + const { channelId } = useParams<{ channelId: string }>(); + const { data: messagesData = [], isLoading, error } = useMessageHistory(channelId); + const [localMessages, setLocalMessages] = useState([]); - const { sendMessage } = useChat((msg) => { - setMessages((prev) => [...prev, msg]); - }); + useEffect(() => { + setLocalMessages([]); + }, [channelId]); + + useEffect(() => { + if (Array.isArray(messagesData) && messagesData.length > 0) { + setLocalMessages(messagesData); + } + }, [messagesData]); + + const handleNewMessage = useCallback((msg: ChatMessage) => { + setLocalMessages(prev => { + if (msg.tempId) { + return prev.map(m => + m.tempId === msg.tempId + ? { ...msg, status: 'sent' as const } + : m + ); + } + return [...prev, msg]; + }); + }, []); + + + const { sendMessage } = useChat(channelId, handleNewMessage); const uploadImage = async (file: File): Promise => { const formData = new FormData(); - formData.append('file', file); + formData.append("file", file); try { - const response = await fetch('/api/upload', { - method: 'POST', - body: formData, - }); - const data = await response.json(); - return data.url; + return await postImage(formData); } catch (error) { - console.error('이미지 업로드 실패:', error); + console.error("이미지 업로드 실패:", error); throw error; } }; const handleSend = async (text: string, files?: File[]) => { - let imageUrls: string[] = []; + let imageUrls: string[] = []; - if (files && files.length > 0) { - const uploadPromises = files.map(file => uploadImage(file)); - imageUrls = await Promise.all(uploadPromises); - } + if (files && files.length > 0) { + const uploadPromises = files.map((file) => uploadImage(file)); + imageUrls = await Promise.all(uploadPromises); + } + + const tempMessage: ChatMessage = { + tempId: uuidv4(), + sender: { + memberId: MY_ID, + name: "tester", + avatarUrl: "", + }, + content: text, + createdAt: new Date().toISOString(), + isUpdated: false, + isDeleted: false, + status: 'pending', + attachments: imageUrls.length > 0 + ? imageUrls.map((url) => ({type: "image" as const, url})) + : [], + messageId: 0 + }; + + setLocalMessages(prev => [...prev, tempMessage]); - const msg: ChatMessage = { - chatId: CHAT_ID, + try { + await sendMessage(text, tempMessage.attachments); + } catch (error) { + setLocalMessages(prev => + prev.map(msg => + msg.tempId === tempMessage.tempId + ? { ...msg, status: 'error' as const } + : msg + ) + ); + console.error('메시지 전송 실패:', error); + } + + const msg: Omit = { sender: { memberId: MY_ID, - username: 'tester', - avatarUrl: '', + name: "tester", + avatarUrl: "", }, content: text, createdAt: new Date().toISOString(), + isUpdated: false, + isDeleted: false, attachments: imageUrls.length > 0 - ? imageUrls.map((url) => ({ type: 'image', url })) - : undefined, + ? imageUrls.map((url) => ({ type: "image" as const, url })) + : [], }; - sendMessage(msg.content, msg.attachments); }; + if (!channelId) return
채널 ID가 유효하지 않습니다.
; + if (isLoading) return
로딩 중...
; + if (error) return
에러 발생: {error.message}
; + return (
- +
); diff --git a/src/view/pages/chat/components/ChatView.tsx b/src/view/pages/chat/components/ChatView.tsx deleted file mode 100644 index 2d5bfc5..0000000 --- a/src/view/pages/chat/components/ChatView.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { ChatMessage } from '@service/feature/chat/schema/messageSchema.ts'; -import { ChatMessageItem } from '@pages/chat/components/message/ChatMessageItem.tsx'; - -export const ChatView = ({ - messages, - myId, - }: { - messages: ChatMessage[]; - myId: string; -}) => { - const bottomRef = useRef(null); - - useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages]); - - return ( -
- {messages.map((msg, index) => { - const prev = messages[index - 1]; - const isSameSender = prev?.sender === msg.sender; - const showMeta = !isSameSender; - - return ( - - ); - })} -
-
- ); -}; \ No newline at end of file diff --git a/src/view/pages/chat/components/layout/ChatView.tsx b/src/view/pages/chat/components/layout/ChatView.tsx index 41380e3..7e815d1 100644 --- a/src/view/pages/chat/components/layout/ChatView.tsx +++ b/src/view/pages/chat/components/layout/ChatView.tsx @@ -3,10 +3,7 @@ import { ChatMessage } from '@service/feature/chat/schema/messageSchema.ts'; import { DateDivider } from '@pages/chat/components/message/DateDivider.tsx'; import { ChatMessageItem } from '@pages/chat/components/message/ChatMessageItem.tsx'; -export const ChatView = ({ - messages, - myId, -}: { +export const ChatView = ({messages = [], myId }: { messages: ChatMessage[]; myId: string; }) => { @@ -16,37 +13,30 @@ export const ChatView = ({ bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); + const messageList = Array.isArray(messages) ? messages : []; + const shouldShowDateDivider = (currentMsg: ChatMessage, prevMsg?: ChatMessage) => { if (!prevMsg) return true; - const currentDate = new Date(currentMsg.createdAt); const prevDate = new Date(prevMsg.createdAt); - return currentDate.toDateString() !== prevDate.toDateString(); }; return ( -
- {messages.map((msg, index) => { - const prev = messages[index - 1]; - const isSameSender = prev?.sender === msg.sender; - const showMeta = !isSameSender || shouldShowDateDivider(msg, prev); +
+ {messageList.map((msg, index) => { + const prev = messageList[index - 1]; + const isSameSender = prev?.sender?.memberId === msg.sender?.memberId; + const showMeta = !isSameSender || shouldShowDateDivider(msg, prev); - return ( - <> - {shouldShowDateDivider(msg, prev) && ( - - )} - - - ); - })} -
-
+ return ( +
+ {shouldShowDateDivider(msg, prev) && ()} + +
+ ); + })} +
+
); -}; \ No newline at end of file +}; diff --git a/src/view/pages/chat/components/message/ChatMessageItem.tsx b/src/view/pages/chat/components/message/ChatMessageItem.tsx index a0147c1..b071784 100644 --- a/src/view/pages/chat/components/message/ChatMessageItem.tsx +++ b/src/view/pages/chat/components/message/ChatMessageItem.tsx @@ -1,7 +1,8 @@ import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import { FileIcon } from 'lucide-react'; -import { ChatMessage } from '@service/feature/chat/schema/messageSchema.ts'; +import fallbackIcon from '@assets/img/logo/chatflow.png'; +import { type ChatMessage } from '@service/feature/chat/schema/messageSchema.ts'; dayjs.extend(relativeTime); @@ -11,6 +12,21 @@ interface Props { showMeta: boolean; } +const MessageStatus = ({ status }: { status?: string }) => { + if (!status || status === 'sent') return null; + + return ( + + {status === 'pending' && ( + + )} + {status === 'error' && ( + ⚠️ + )} + + ); +}; + export const ChatMessageItem = ({ msg, isMine, showMeta }: Props) => { const renderAttachment = (attachment: { type: string; url: string }) => { if (attachment.type === 'image') { @@ -43,22 +59,24 @@ export const ChatMessageItem = ({ msg, isMine, showMeta }: Props) => {
{!isMine && showMeta && ( {msg.sender.username} { e.currentTarget.src = fallbackIcon; }} /> )}
{showMeta && (
- {msg.sender.username} + {msg.sender.name} {dayjs(msg.createdAt).fromNow()}
)} +
{msg.content && (

{msg.content}