veza/apps/web/src/features/chat/components/ChatRoom.tsx
senke 45141b9ee4 aesthetic-improvements: reduce decorative cyan in error and chat components (80/20 rule, batch 6)
- NotFoundPage: decorative icon background (bg-kodo-cyan/20 → bg-kodo-steel/20, icon text-kodo-cyan → text-kodo-steel)
- ServerErrorPage: informational status box (bg-kodo-cyan/10 → bg-kodo-steel/10, border-kodo-cyan → border-kodo-steel, icon/text text-kodo-cyan → text-kodo-steel)
- ChatRoom: empty state icon background (bg-kodo-cyan/10 → bg-kodo-steel/10, border-kodo-cyan/20 → border-kodo-steel/20, icon text-kodo-cyan → text-kodo-steel)
- PlaybackHeatmap: stats box background (bg-kodo-cyan/10 → bg-kodo-steel/10, text-kodo-cyan → text-kodo-steel)
- Total: ~4 files, ~5 instances replaced
- Preserved: Active/functional states (ChatInput drag active overlay, ChatRoom highlighted message, TrackFilters active filters badge, PlaylistBatchActions batch mode banner, PlaybackHeatmap intensity visualization - functional), semantic status indicators (TrackHistory updated action - semantic color)
- Action 11.3.1.3 in progress (sixth batch: error pages and chat components)
2026-01-16 11:15:52 +01:00

153 lines
5.4 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import { useChatStore } from '../store/chatStore';
import { ChatMessageComponent } from './ChatMessage';
import { useChat } from '../hooks/useChat';
import { MessageSearch } from './MessageSearch';
import { TypingIndicator } from './TypingIndicator';
import { Search, X, Wifi } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface ChatRoomProps {
conversationId: string;
}
export const ChatRoom: React.FC<ChatRoomProps> = ({ conversationId }) => {
const { messages, wsStatus } = useChatStore();
const { fetchHistory } = useChat();
const messagesEndRef = useRef<HTMLDivElement>(null);
const [showSearch, setShowSearch] = useState(false);
const [highlightedMessageId, setHighlightedMessageId] = useState<
string | null
>(null);
const currentMessages = messages[conversationId] || [];
const fetchingRef = useRef<{ [key: string]: boolean }>({});
useEffect(() => {
if (
conversationId &&
!messages[conversationId] &&
!fetchingRef.current[conversationId]
) {
fetchingRef.current[conversationId] = true;
fetchHistory(conversationId).finally(() => {
// Fetch complete
});
}
}, [conversationId, messages[conversationId], fetchHistory]);
useEffect(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [currentMessages.length, conversationId]); // Scroll on new messages or channel switch
const handleMessageSelect = (messageId: string) => {
setHighlightedMessageId(messageId);
const messageElement = document.getElementById(`message-${messageId}`);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => setHighlightedMessageId(null), 3000);
}
};
if (!conversationId) {
return (
<div className="flex-1 flex flex-col items-center justify-center text-kodo-secondary opacity-50 space-y-4">
<div className="w-24 h-24 rounded-full bg-white/5 flex items-center justify-center animate-pulse">
<Wifi className="w-10 h-10 text-kodo-cyan opacity-50" />
</div>
<p className="text-sm font-mono uppercase tracking-widest">
Awaiting Frequency Selection
</p>
</div>
);
}
return (
<div className="flex-1 flex flex-col h-full overflow-hidden">
{/* Search Header Overlay */}
<div
className={cn(
'absolute top-0 left-0 right-0 z-20 px-4 py-2 transition-all duration-300',
showSearch
? 'bg-kodo-void/90 backdrop-blur-md border-b border-white/10'
: 'bg-transparent pointer-events-none',
)}
>
{showSearch ? (
<div className="flex items-center gap-2 max-w-2xl mx-auto">
<div className="flex-1">
<MessageSearch
conversationId={conversationId}
onMessageSelect={handleMessageSelect}
/>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowSearch(false)}
className="hover:bg-white/10"
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<div className="flex justify-end pointer-events-auto">
<Button
variant="ghost"
size="sm"
onClick={() => setShowSearch(true)}
className="text-kodo-secondary/50 hover:text-white hover:bg-white/5 bg-black/20 backdrop-blur-sm rounded-full h-8 px-4 border border-white/5"
>
<Search className="h-3 w-3 mr-2" />
<span className="text-xs font-mono uppercase">Search Log</span>
</Button>
</div>
)}
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 space-y-4 scroll-smooth">
{/* Welcome Message for Empty Room */}
{currentMessages.length === 0 && (
<div className="flex flex-col items-center justify-center h-[50vh] text-center space-y-3 opacity-60">
<div className="w-12 h-12 rounded-xl bg-kodo-steel/10 flex items-center justify-center border border-kodo-steel/20">
<MessageSquare className="w-6 h-6 text-kodo-steel" />
</div>
<div>
<p className="text-white font-medium">Channel Established</p>
<p className="text-sm text-kodo-secondary mt-1">
Begin transmission on this frequency.
</p>
</div>
</div>
)}
{/* Message Stream */}
{currentMessages.map((msg, index) => {
const isMe = false; // TODO: Check with current user ID from store
const isSequence =
index > 0 && currentMessages[index - 1].sender_id === msg.sender_id;
return (
<div
key={msg.id}
id={`message-${msg.id}`}
className={cn(
'transition-all duration-500 animate-slideUp',
highlightedMessageId === msg.id &&
'bg-kodo-cyan/10 rounded-xl -mx-4 px-4 py-2 ring-1 ring-kodo-cyan/30',
)}
>
<ChatMessageComponent message={msg} />
</div>
);
})}
<TypingIndicator conversationId={conversationId} />
<div ref={messagesEndRef} className="h-4" />
</div>
</div>
);
};