diff --git a/apps/main/src/components/ChatMessages.tsx b/apps/main/src/components/ChatMessages.tsx new file mode 100644 index 0000000..69fb22c --- /dev/null +++ b/apps/main/src/components/ChatMessages.tsx @@ -0,0 +1,183 @@ +import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; +import { + Avatar, + ChatContainer, + Message, + MessageInput, + MessageList, + MessageSeparator, + TypingIndicator, +} from "@chatscope/chat-ui-kit-react"; +import { useMemo } from "react"; + +interface ChatMessage { + id: string; + userId: string; + text: string; + createdAt: string; + clientId: string; + optimistic?: boolean; +} + +interface Member { + id: string; + name: string; + avatar_url: string | null; +} + +interface ChatMessagesProps { + messages: ChatMessage[]; + currentUserId: string; + members: Member[]; + typingUsers: string[]; + hasMoreMessages: boolean; + onLoadMore?: () => void; + onSend: (text: string) => void; + onTyping: () => void; + placeholder?: string; +} + +function formatDateSeparator(dateStr: string): string { + const date = new Date(dateStr); + const now = new Date(); + const isToday = date.toDateString() === now.toDateString(); + + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + const isYesterday = date.toDateString() === yesterday.toDateString(); + + if (isToday) return "Aujourd'hui"; + if (isYesterday) return "Hier"; + + return date.toLocaleDateString("fr-FR", { + weekday: "long", + day: "numeric", + month: "long", + year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, + }); +} + +function formatTime(dateStr: string): string { + return new Date(dateStr).toLocaleTimeString("fr-FR", { + hour: "2-digit", + minute: "2-digit", + }); +} + +function getInitials(name: string): string { + return name + .split(" ") + .map((w) => w[0]) + .join("") + .toUpperCase() + .slice(0, 2); +} + +export function ChatMessages({ + messages, + currentUserId, + members, + typingUsers, + hasMoreMessages, + onLoadMore, + onSend, + onTyping, + placeholder = "Envoyer un message...", +}: ChatMessagesProps) { + const membersById = useMemo(() => { + const map = new Map(); + for (const m of members) { + map.set(m.id, m); + } + return map; + }, [members]); + + const getMemberName = (userId: string) => + membersById.get(userId)?.name ?? "Utilisateur"; + + const typingContent = useMemo(() => { + if (typingUsers.length === 0) return null; + const names = typingUsers.map(getMemberName); + if (names.length === 1) return `${names[0]} écrit...`; + return `${names.join(", ")} écrivent...`; + }, [typingUsers, membersById]); + + // Build messages with date separators + const elements = useMemo(() => { + const result: React.ReactNode[] = []; + let lastDate = ""; + + for (const msg of messages) { + const msgDate = new Date(msg.createdAt).toDateString(); + if (msgDate !== lastDate) { + lastDate = msgDate; + result.push( + + {formatDateSeparator(msg.createdAt)} + + ); + } + + const isOwn = msg.userId === currentUserId; + const sender = getMemberName(msg.userId); + const member = membersById.get(msg.userId); + + result.push( + + {!isOwn && ( + + {!member?.avatar_url && ( +
+ {getInitials(sender)} +
+ )} +
+ )} + +
+ ); + } + + return result; + }, [messages, currentUserId, membersById]); + + const handleSend = (_innerHtml: string, textContent: string) => { + const text = textContent.trim(); + if (!text) return; + onSend(text); + }; + + return ( + + : undefined + } + onYReachStart={hasMoreMessages ? onLoadMore : undefined} + > + {elements} + + + + ); +} diff --git a/apps/main/src/components/TabloDiscussionSection.tsx b/apps/main/src/components/TabloDiscussionSection.tsx index 70a1d1b..c191485 100644 --- a/apps/main/src/components/TabloDiscussionSection.tsx +++ b/apps/main/src/components/TabloDiscussionSection.tsx @@ -1,15 +1,9 @@ -import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; -import { - ChatContainer, - Message, - MessageInput, - MessageList, - TypingIndicator, -} from "@chatscope/chat-ui-kit-react"; import { UserTablo } from "@xtablo/shared/types/tablos.types"; import { useEffect } from "react"; import { useChat } from "../hooks/useChat"; +import { useTabloMembers } from "../hooks/tablos"; import { useUser } from "../providers/UserStoreProvider"; +import { ChatMessages } from "./ChatMessages"; import { TabloHeaderActions } from "./TabloHeaderActions"; interface TabloDiscussionSectionProps { @@ -29,6 +23,8 @@ export const TabloDiscussionSection = ({ tablo, isAdmin }: TabloDiscussionSectio markAsRead, } = useChat(tablo.id); + const { data: members = [] } = useTabloMembers(tablo.id); + // Mark as read when opening the discussion useEffect(() => { if (messages.length > 0) { @@ -36,15 +32,9 @@ export const TabloDiscussionSection = ({ tablo, isAdmin }: TabloDiscussionSectio } }, [messages.length, markAsRead]); - const handleSend = (_innerHtml: string, textContent: string) => { - const text = textContent.trim(); - if (!text) return; - sendMessage(text); - }; - return (
-
+

Discussion

Conversations liées à ce tablo

@@ -53,34 +43,16 @@ export const TabloDiscussionSection = ({ tablo, isAdmin }: TabloDiscussionSectio
- - 0 ? ( - - ) : null - } - onYReachStart={hasMoreMessages ? loadMoreMessages : undefined} - > - {messages.map((msg) => ( - - ))} - - - +
); diff --git a/apps/main/src/pages/chat.test.tsx b/apps/main/src/pages/chat.test.tsx index abff11f..dbc8780 100644 --- a/apps/main/src/pages/chat.test.tsx +++ b/apps/main/src/pages/chat.test.tsx @@ -32,6 +32,7 @@ vi.mock("../hooks/tablos", () => ({ { id: "tablo-2", name: "Test Tablo 2" }, ], }), + useTabloMembers: () => ({ data: [] }), })); describe("ChatPage", () => { diff --git a/apps/main/src/pages/chat.tsx b/apps/main/src/pages/chat.tsx index 66f8708..ea61187 100644 --- a/apps/main/src/pages/chat.tsx +++ b/apps/main/src/pages/chat.tsx @@ -1,18 +1,11 @@ -import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; -import { - ChatContainer, - MessageList, - Message, - MessageInput, - TypingIndicator, -} from "@chatscope/chat-ui-kit-react"; import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ChatChannelPreview } from "../components/ChatChannelPreview"; import { ChatHeader } from "../components/ChatHeader"; +import { ChatMessages } from "../components/ChatMessages"; import { useChat } from "../hooks/useChat"; import { useChatUnread } from "../hooks/useChatUnread"; -import { useTablosList } from "../hooks/tablos"; +import { useTablosList, useTabloMembers } from "../hooks/tablos"; import { useUser } from "../providers/UserStoreProvider"; export function ChatPage() { @@ -29,9 +22,12 @@ export function ChatPage() { sendTyping, typingUsers, onlineUsers, + loadMoreMessages, + hasMoreMessages, markAsRead, } = useChat(channelId); + const { data: members = [] } = useTabloMembers(channelId ?? ""); const activeTablo = tablos?.find((t) => t.id === channelId) ?? null; // Mark as read when channel is focused @@ -41,12 +37,6 @@ export function ChatPage() { } }, [channelId, messages.length, markAsRead]); - const handleSend = (_innerHtml: string, textContent: string) => { - const text = textContent.trim(); - if (!text) return; - sendMessage(text); - }; - const handleChannelSelect = (tabloId: string) => { navigate(`/chat/${tabloId}`); }; @@ -88,34 +78,17 @@ export function ChatPage() { onlineUsers={onlineUsers} />
- - 0 ? ( - - ) : undefined - } - > - {messages.map((msg) => ( - - ))} - - sendTyping()} - attachButton={false} - /> - +
) : (