import { useEffect, useRef, useState, useMemo } from "react"; import { useMessageStore } from "../stores/message.ts"; import { useChannelStore } from "../stores/channel.ts"; import { useServerStore } from "../stores/server.ts"; import { useMemberStore } from "../stores/member.ts"; import { useThreadStore } from "../stores/thread.ts"; import { GiphyPicker, type Gif } from "./GiphyPicker"; import { MentionDropdown } from "./MentionDropdown"; import { useReadStatesStore } from "../stores/readStates.ts"; import { MessageSearch } from "./MessageSearch"; import { ThreadListPanel } from "./ThreadListPanel.tsx"; import { ThreadPanel } from "./ThreadPanel.tsx"; import { ForumView } from "./ForumView.tsx"; import { CalendarView } from "./CalendarView.tsx"; import { DocsView } from "./DocsView.tsx"; import { ListView } from "./ListView.tsx"; import { UserProfileModal } from "./UserProfileModal.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 renderContent(content: string, memberUsernames: Set) { const segments: { type: "text" | "mention"; value: string }[] = []; const mentionRe = /@([a-zA-Z0-9_.-]+)/g; let last = 0; let match: RegExpExecArray | null; while ((match = mentionRe.exec(content)) !== null) { if (match.index > last) { segments.push({ type: "text", value: content.slice(last, match.index) }); } const username = match[1]; if (memberUsernames.has(username)) { segments.push({ type: "mention", value: username }); } else { segments.push({ type: "text", value: match[0] }); } last = mentionRe.lastIndex; } if (last < content.length) { segments.push({ type: "text", value: content.slice(last) }); } return segments.map((seg, idx) => { if (seg.type === "mention") { return ( @{seg.value} ); } return ( , code: ({ ...props }) => , pre: ({ ...props }) =>
,
          blockquote: ({ ...props }) => 
, table: ({ ...props }) => , th: ({ ...props }) =>
, td: ({ ...props }) => , p: ({ ...props }) => , }} > {seg.value} ); }); } function renderEmbeds(embeds?: { url: string; title?: string; description?: string; image_url?: string; site_name?: string }[]) { if (!embeds || embeds.length === 0) return null; return ( ); } export function ChatArea() { const activeChannelId = useChannelStore((s) => s.activeChannelId); const channelsByServer = useChannelStore((s) => s.channelsByServer); const activeServerId = useServerStore((s) => s.activeServerId); const messages = useMessageStore((s) => activeChannelId ? s.messagesByChannel[activeChannelId] || [] : [], ); const isLoading = useMessageStore((s) => s.isLoading); const fetchMessages = useMessageStore((s) => s.fetchMessages); const sendMessage = useMessageStore((s) => s.sendMessage); const membersByServer = useMemberStore((s) => s.membersByServer); const threads = useThreadStore((s) => activeChannelId ? s.threadsByParent[activeChannelId] || [] : []); const [input, setInput] = useState(""); const [error, setError] = useState(null); const [showGifPicker, setShowGifPicker] = useState(false); const [showSearch, setShowSearch] = useState(false); const [mentionQuery, setMentionQuery] = useState(null); const [slowmodeRemaining, setSlowmodeRemaining] = useState(0); const [selectMode, setSelectMode] = useState(false); const [showThreads, setShowThreads] = useState(false); const [activeThread, setActiveThread] = useState<{ id: string; name: string } | null>(null); const [profileUserId, setProfileUserId] = useState(null); const bottomRef = useRef(null); const inputRef = useRef(null); const toggleSelectedMessage = useMessageStore((s) => s.toggleSelectedMessage); const clearSelectedMessages = useMessageStore((s) => s.clearSelectedMessages); const bulkDeleteMessages = useMessageStore((s) => s.bulkDeleteMessages); const selectedIds = useMessageStore((s) => activeChannelId ? s.selectedMessageIds[activeChannelId] || new Set() : new Set(), ); const canBulkDelete = true; const channels = activeServerId ? channelsByServer[activeServerId] || [] : []; const activeChannel = channels.find((c) => c.id === activeChannelId); const members = activeServerId ? membersByServer[activeServerId] || [] : []; const memberUsernames = useMemo(() => new Set(members.map((m) => m.username)), [members]); const markRead = useReadStatesStore((s) => s.markRead); useEffect(() => { if (activeChannelId) { fetchMessages(activeChannelId); setError(null); } }, [activeChannelId, fetchMessages]); useEffect(() => { if (!activeChannelId || messages.length === 0 || isLoading) return; const lastMsg = messages[messages.length - 1]; if (lastMsg?.id) { markRead(activeChannelId, lastMsg.id); } }, [activeChannelId, messages.length, isLoading]); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "auto" }); }, [messages]); useEffect(() => { if (slowmodeRemaining <= 0) return; const t = setInterval(() => { setSlowmodeRemaining((r) => { if (r <= 1) { clearInterval(t); return 0; } return r - 1; }); }, 1000); return () => clearInterval(t); }, [slowmodeRemaining]); const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; const cursor = e.target.selectionStart ?? value.length; setInput(value); const beforeCursor = value.slice(0, cursor); const atIndex = beforeCursor.lastIndexOf("@"); if (atIndex === -1) { setMentionQuery(null); return; } const between = beforeCursor.slice(atIndex + 1); if (between.includes(" ") || between.includes("\n")) { setMentionQuery(null); return; } setMentionQuery(between); }; const handleMentionSelect = (username: string) => { const inputEl = inputRef.current; if (!inputEl) { setInput((prev) => `${prev}${username} `); setMentionQuery(null); return; } const cursor = inputEl.selectionStart ?? input.length; const beforeCursor = input.slice(0, cursor); const atIndex = beforeCursor.lastIndexOf("@"); if (atIndex === -1) return; const before = input.slice(0, atIndex); const after = input.slice(cursor); const next = `${before}@${username} ${after}`; setInput(next); setMentionQuery(null); requestAnimationFrame(() => { const pos = atIndex + username.length + 2; inputEl.focus(); inputEl.setSelectionRange(pos, pos); }); }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!activeChannelId || !input.trim() || slowmodeRemaining > 0) return; setError(null); try { await sendMessage(activeChannelId, input.trim()); setInput(""); setMentionQuery(null); } catch (err) { const msg = err instanceof Error ? err.message : "Failed to send"; try { const parsed = JSON.parse(msg); if (parsed.error === "slowmode" && typeof parsed.retry_after === "number") { setSlowmodeRemaining(parsed.retry_after); return; } } catch { // not json } setError(msg); } }; const handleGifSelect = async (gif: Gif) => { if (!activeChannelId) return; const content = `![${gif.title || "GIF"}](${gif.url})`; try { await sendMessage(activeChannelId, content); setShowGifPicker(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to send"); } }; return (
{activeChannel ? activeChannel.type === 'forum' ? `■ ${activeChannel.name}` : activeChannel.type === 'calendar' ? `○ ${activeChannel.name}` : activeChannel.type === 'docs' ? `☰ ${activeChannel.name}` : activeChannel.type === 'list' ? `☑ ${activeChannel.name}` : `# ${activeChannel.name}` : activeChannelId ? `#${activeChannelId}` : "[NO CHANNEL SELECTED]"} {activeChannelId && activeChannel?.type === 'text' && (
)}
{showSearch && activeChannelId && activeChannel && ( setShowSearch(false)} /> )} {showThreads && activeChannelId && activeChannel?.type !== 'forum' && ( setActiveThread(t)} onClose={() => setShowThreads(false)} /> )} {selectMode && activeChannelId && activeChannel?.type !== 'forum' && (
{selectedIds.size} selected
)}
{activeChannel && activeChannel.type === 'forum' && activeChannelId ? ( setActiveThread(t)} /> ) : activeChannel && activeChannel.type === 'calendar' && activeChannelId ? ( ) : activeChannel && activeChannel.type === 'docs' && activeChannelId ? ( ) : activeChannel && activeChannel.type === 'list' && activeChannelId ? ( ) : ( <>
{isLoading &&

[loading...]

} {!isLoading && messages.length === 0 && (

[no messages in this channel]

)} {messages.map((message) => (
selectMode && activeChannelId && toggleSelectedMessage(activeChannelId, message.id)} className={`break-words ${selectMode ? 'cursor-pointer hover:bg-gb-bg-s' : ''} ${selectedIds.has(message.id) ? 'bg-gb-bg-s' : ''}`} > {selectMode && activeChannelId && ( {selectedIds.has(message.id) ? '[x]' : '[ ]'} )} [{formatTime(message.created_at)}]{" "} setProfileUserId(message.author_id)}> <{message.author_username}> {" "} {renderContent(message.content, memberUsernames)} {renderEmbeds(message.embeds)}
))}
{showGifPicker && (
setShowGifPicker(false)} />
)} {error && (

ERR: {error}

)} {slowmodeRemaining > 0 && (
SLOWMODE: wait {slowmodeRemaining}s
)}
{">"}
0} /> {mentionQuery !== null && ( )}
{activeThread && ( setActiveThread(null)} /> )} )}
{profileUserId && ( setProfileUserId(null)} /> )}
); }