import { useEffect, useRef, useState, useCallback, memo } from "react"; import { useParams } from "react-router-dom"; import { useConversationStore, type ConversationMessage } from "../stores/conversation.ts"; import { useAuthStore } from "../stores/auth.ts"; import { useTypingStore } from "../stores/typing.ts"; import { useLayoutStore } from "../stores/layout.ts"; import { GiphyPicker, type Gif } from "./GiphyPicker.tsx"; import { EmojiPicker } from "./EmojiPicker.tsx"; import { FormatToolbar } from "./FormatToolbar.tsx"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; function formatTime(iso: string): string { const date = new Date(iso); const hours = date.getHours().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0"); return `${hours}:${minutes}`; } function renderDMContent(content: string) { return ( , code: ({ ...props }) => , pre: ({ ...props }) =>
,
        blockquote: ({ ...props }) => 
, p: ({ ...props }) => , img: ({ src, ...props }) => { const finalSrc = src?.startsWith("https://media") && src.includes(".giphy.com/") ? `/api/v1/gifs/proxy?url=${encodeURIComponent(src)}` : src; return ( ); }, }} > {content} ); } const DMMessageItem = memo(({ msg }: { msg: ConversationMessage }) => { return (
[{formatTime(msg.created_at)}]{" "} <{msg.author_username}>{" "} {renderDMContent(msg.content)}
); }); DMMessageItem.displayName = "DMMessageItem"; export function DMChat() { const { conversationId } = useParams<{ conversationId: string }>(); const activeId = useConversationStore((s) => s.activeConversationId); const setActive = useConversationStore((s) => s.setActiveConversation); const conversations = useConversationStore((s) => s.conversations); const messagesByConv = useConversationStore((s) => s.messagesByConversation); const fetchMessages = useConversationStore((s) => s.fetchMessages); const fetchOlderMessages = useConversationStore((s) => s.fetchOlderMessages); const isLoadingOlder = useConversationStore((s) => s.isLoadingOlder); const sendMessage = useConversationStore((s) => s.sendMessage); const fetchConversations = useConversationStore((s) => s.fetchConversations); const currentUser = useAuthStore((s) => s.user); const typingUsers = useTypingStore((s) => s.typingUsers); const sendTypingStart = useTypingStore((s) => s.sendTypingStart); const [input, setInput] = useState(""); const [error, setError] = useState(null); const [showGifPicker, setShowGifPicker] = useState(false); const [showKaomoji, setShowKaomoji] = useState(false); const bottomRef = useRef(null); const scrollContainerRef = useRef(null); const inputRef = useRef(null); const lastTypingRef = useRef(0); const id = conversationId || activeId; const conversation = conversations.find((c) => c.id === id); const handleScroll = useCallback(() => { const el = scrollContainerRef.current; if (!el || !id || isLoadingOlder) return; if (el.scrollTop < 100) { const prevHeight = el.scrollHeight; fetchOlderMessages(id).then(() => { requestAnimationFrame(() => { el.scrollTop = el.scrollHeight - prevHeight; }); }); } }, [id, isLoadingOlder, fetchOlderMessages]); const messages = id ? messagesByConv[id] || [] : []; useEffect(() => { fetchConversations(); }, [fetchConversations]); useEffect(() => { if (id) { setActive(id); fetchMessages(id); setError(null); } }, [id, setActive, fetchMessages]); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "auto" }); }, [messages]); // Ctrl+E kaomoji shortcut useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === 'e') { e.preventDefault(); setShowKaomoji((prev) => !prev); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, []); const handleGifSelect = async (gif: Gif) => { if (!id) return; const content = `![${gif.title || "GIF"}](${gif.images.fixed_height.url})`; try { await sendMessage(id, content); setShowGifPicker(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to send"); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!id || !input.trim()) return; setError(null); try { await sendMessage(id, input.trim()); setInput(""); } catch (err) { setError(err instanceof Error ? err.message : "Failed to send"); } }; const selfDM = conversation && conversation.members.length === 1 && conversation.members[0].id === currentUser?.id; const title = selfDM ? "Notes" : conversation ? conversation.type === "group_dm" ? conversation.name || conversation.members.map((m) => m.username).join(", ") : conversation.members.find((m) => m.id !== currentUser?.id)?.username || "DM" : id ? "[LOADING...]" : "[NO CONVERSATION]"; return (
@ {title}
{isLoadingOlder &&

[loading older messages...]

} {!id &&

[select a conversation]

} {id && messages.length === 0 &&

[no messages]

} {messages.map((msg: ConversationMessage) => ( ))}
{showGifPicker && (
setShowGifPicker(false)} />
)}
{(() => { const convTyping = id ? (typingUsers[id] || []).filter(u => u.userId !== currentUser?.id) : []; if (convTyping.length === 0) return "\u00A0"; const names = convTyping.map(u => u.username); return names.length === 1 ? `${names[0]} is typing...` : names.length === 2 ? `${names[0]} and ${names[1]} are typing...` : `${names[0]} and ${names.length - 1} others are typing...`; })()}
{error && (

ERR: {error}

)}
{">"} { setInput(e.target.value); if (id && e.target.value.length > 0) { const now = Date.now(); if (now - lastTypingRef.current > 3000) { sendTypingStart(id); lastTypingRef.current = now; } } }} placeholder="type a message..." className="terminal-input w-full" disabled={!id} /> {showKaomoji && ( setInput((prev) => prev + emoji)} onClose={() => setShowKaomoji(false)} /> )}
); }