veza/apps/web/src/features/chat/components/ChatRoom.tsx

161 lines
5.6 KiB
TypeScript
Raw Normal View History

import React, { useEffect, useRef, useState } from 'react';
2025-12-13 02:34:34 +00:00
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, Disc,
Clock,
MessageSquare, Wifi
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useUser } from '@/features/auth/hooks/useUser';
interface ChatRoomProps {
conversationId: string;
}
export const ChatRoom: React.FC<ChatRoomProps> = ({ conversationId }) => {
const { messages, wsStatus } = useChatStore();
const { fetchHistory } = useChat();
const { data: user } = useUser();
const currentUserId = user?.id;
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-steel 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-[var(--duration-normal)]',
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)}
aesthetic-improvements: replace secondary cyan hover states with steel - Button outline variant: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - Header secondary nav: hover:text-kodo-cyan → hover:text-white, hover:bg-kodo-cyan/5 → hover:bg-white/5 - FileManagerView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 (kept selected state cyan) - ProjectsManager: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50, hover:text-kodo-cyan → hover:text-white - GroupDetailView: hover:border-kodo-cyan/30 → hover:border-kodo-steel/50 - AIToolsView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - CloudFileBrowser: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 (kept selected state cyan) - ProfileView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - CourseCard: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - TwoFactorSetup: hover:border-kodo-cyan → hover:border-kodo-steel/50 - GearView: hover:text-kodo-cyan → hover:text-white, hover:border-kodo-cyan → hover:border-kodo-steel/50 - ChatInput: hover:text-kodo-cyan → hover:text-white (3 instances) - ChatMessage: hover:text-kodo-cyan → hover:text-white (2 instances) - ChatRoom: hover:text-kodo-cyan → hover:text-white - AddToPlaylistModal: hover:border-kodo-cyan → hover:border-kodo-steel/50, hover:text-kodo-cyan → hover:text-white - Preserved focus rings (cyan) and active/selected states (cyan) as per audit - Action 11.3.1.2 in progress (first batch of ~15 files)
2026-01-16 09:51:30 +00:00
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-4 opacity-60">
<div className="w-12 h-12 rounded-xl bg-muted/10 flex items-center justify-center border border-border/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 = currentUserId ? msg.sender_id === currentUserId : false;
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-[var(--duration-slow)] animate-slideUp',
highlightedMessageId === msg.id &&
'bg-muted/10 rounded-xl -mx-4 px-4 py-2 ring-1 ring-kodo-steel/30',
)}
>
<ChatMessageComponent message={msg} />
</div>
);
})}
<TypingIndicator conversationId={conversationId} />
<div ref={messagesEndRef} className="h-4" />
</div>
</div>
);
};