import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth'; import { getFirestore, collection, addDoc, query, // orderBy, // We will sort in client for DMs due to composite query needs onSnapshot, serverTimestamp, doc, setDoc, getDoc, Timestamp, where, getDocs, orderBy // Keep for channel messages and initial channel list } from 'firebase/firestore'; import { setLogLevel } from 'firebase/app'; // --- Icon Components --- const HashIcon = () => ( ); const PlusIcon = () => ( ); const SendIcon = () => ( ); const UsersIcon = () => ( ); const MessageSquareIcon = () => ( ); // --- End Icon Components --- // --- Firebase Configuration --- const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : { apiKey: "YOUR_API_KEY", authDomain: "YOUR_AUTH_DOMAIN", projectId: "YOUR_PROJECT_ID", storageBucket: "YOUR_STORAGE_BUCKET", messagingSenderId: "YOUR_MESSAGING_SENDER_ID", appId: "YOUR_APP_ID" }; const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-chat-app-dm'; // Initialize Firebase let app; let auth; let db; try { app = initializeApp(firebaseConfig); auth = getAuth(app); db = getFirestore(app); setLogLevel('debug'); } catch (error) { console.error("Error initializing Firebase:", error); } const App = () => { const [currentUser, setCurrentUser] = useState(null); const [userId, setUserId] = useState(null); const [userName, setUserName] = useState(''); const [isUserNameSet, setIsUserNameSet] = useState(false); const [tempUserName, setTempUserName] = useState(''); // Group Channels state const [channels, setChannels] = useState([]); const [selectedChannelId, setSelectedChannelId] = useState(null); const [selectedChannelName, setSelectedChannelName] = useState(''); const [newChannelName, setNewChannelName] = useState(''); // Direct Messages (DM) state const [allUsers, setAllUsers] = useState([]); // For starting new DMs const [dmConversations, setDmConversations] = useState([]); // Active DM conversations const [selectedDmConversationId, setSelectedDmConversationId] = useState(null); const [selectedDmPeer, setSelectedDmPeer] = useState(null); // { id: string, name: string } const [showUserListModal, setShowUserListModal] = useState(false); // Common state const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); const [chatMode, setChatMode] = useState('channel'); // 'channel' or 'dm' const [isLoadingChannels, setIsLoadingChannels] = useState(true); const [isLoadingMessages, setIsLoadingMessages] = useState(false); const [isLoadingUsers, setIsLoadingUsers] = useState(false); const [isLoadingDms, setIsLoadingDms] = useState(false); const [error, setError] = useState(null); const messagesEndRef = useRef(null); // Firestore Paths const CHANNELS_PATH = `artifacts/${appId}/public/data/chat_channels`; const USER_PROFILES_PATH = `artifacts/${appId}/public/data/user_profiles`; const DM_CONVERSATIONS_PATH = `artifacts/${appId}/public/data/dm_conversations`; const getGroupChannelMessagesPath = (channelId) => `artifacts/${appId}/public/data/chat_channels/${channelId}/messages`; const getDmMessagesPath = (dmConversationId) => `artifacts/${appId}/public/data/dm_conversations/${dmConversationId}/messages`; const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [messages]); // --- Auth and User Profile --- useEffect(() => { if (!auth) { setError("Firebase Auth not initialized."); return; } const unsubscribe = onAuthStateChanged(auth, async (user) => { if (user) { setCurrentUser(user); const uId = user.uid; setUserId(uId); console.log("User authenticated with UID:", uId); try { const userProfileRef = doc(db, USER_PROFILES_PATH, uId); const docSnap = await getDoc(userProfileRef); if (docSnap.exists() && docSnap.data().displayName) { setUserName(docSnap.data().displayName); setIsUserNameSet(true); console.log("Username loaded:", docSnap.data().displayName); } else { setIsUserNameSet(false); console.log("No user profile/displayName found for UID:", uId, "Prompting."); } } catch (e) { console.error("Error fetching user profile:", e); setError("Error fetching user profile."); setIsUserNameSet(false); } } else { setCurrentUser(null); setUserId(null); setUserName(''); setIsUserNameSet(false); console.log("User not authenticated, attempting to sign in."); try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (e) { console.error("Error during sign-in:", e); setError("Failed to sign in."); } } }); return () => unsubscribe(); }, []); const handleSetUserName = async () => { if (tempUserName.trim() && userId && db) { try { const userProfileRef = doc(db, USER_PROFILES_PATH, userId); await setDoc(userProfileRef, { displayName: tempUserName.trim(), uid: userId }, { merge: true }); setUserName(tempUserName.trim()); setIsUserNameSet(true); setTempUserName(''); setError(null); } catch (e) { console.error("Error saving username:", e); setError("Failed to save username."); } } else { setError("Display name cannot be empty or user/db not ready."); } }; // --- Fetch Group Channels --- useEffect(() => { if (!db || !isUserNameSet) { setIsLoadingChannels(!!isUserNameSet); return; } setIsLoadingChannels(true); const q = query(collection(db, CHANNELS_PATH), orderBy("createdAt", "asc")); const unsubscribe = onSnapshot(q, (snapshot) => { const channelsData = snapshot.docs.map(d => ({ id: d.id, ...d.data() })); setChannels(channelsData); setIsLoadingChannels(false); if (channelsData.length > 0 && !selectedChannelId && !selectedDmConversationId) { // Default to first channel if nothing else selected setSelectedChannelId(channelsData[0].id); setSelectedChannelName(channelsData[0].name); setChatMode('channel'); } else if (channelsData.length === 0 && !selectedDmConversationId) { setSelectedChannelId(null); setSelectedChannelName(''); } }, (err) => { console.error("Error fetching channels:", err); setError("Failed to load channels."); setIsLoadingChannels(false); }); return () => unsubscribe(); }, [db, isUserNameSet]); // --- Fetch All Users (for starting DMs) --- useEffect(() => { if (!db || !isUserNameSet) { setIsLoadingUsers(!!isUserNameSet); return; } setIsLoadingUsers(true); const usersCollectionRef = collection(db, USER_PROFILES_PATH); const unsubscribe = onSnapshot(usersCollectionRef, (snapshot) => { const usersData = snapshot.docs .map(d => ({ id: d.id, ...d.data() })) .filter(u => u.id !== userId && u.displayName); // Exclude self and users without display names setAllUsers(usersData); setIsLoadingUsers(false); }, (err) => { console.error("Error fetching users:", err); setError("Failed to load users."); setIsLoadingUsers(false); }); return () => unsubscribe(); }, [db, userId, isUserNameSet]); // --- Fetch DM Conversations --- useEffect(() => { if (!db || !userId || !isUserNameSet) { setIsLoadingDms(!!(userId && isUserNameSet)); return; } setIsLoadingDms(true); const dmQuery = query(collection(db, DM_CONVERSATIONS_PATH), where("participants", "array-contains", userId)); const unsubscribe = onSnapshot(dmQuery, async (snapshot) => { const convosData = snapshot.docs.map(d => ({ id: d.id, ...d.data() })); // Enhance with peer information const enhancedConvos = await Promise.all(convosData.map(async (convo) => { const peerId = convo.participants.find(pId => pId !== userId); if (!peerId) return { ...convo, peerName: "Unknown User", peerId: null }; let peerName = "Loading..."; // Try to get peer name from convo document first if (convo.participantNames && convo.participantNames[peerId]) { peerName = convo.participantNames[peerId]; } else { // Fallback to fetching profile try { const userProfileRef = doc(db, USER_PROFILES_PATH, peerId); const docSnap = await getDoc(userProfileRef); if (docSnap.exists() && docSnap.data().displayName) { peerName = docSnap.data().displayName; } else { peerName = "Unknown User"; } } catch (e) { console.warn("Could not fetch peer profile for DM list:", peerId, e); peerName = "Unknown User"; } } return { ...convo, peerName, peerId }; })); // Sort by lastMessageAt (client-side as Firestore multiple array-contains + orderBy is tricky) enhancedConvos.sort((a, b) => { const timeA = a.lastMessageAt ? (a.lastMessageAt.seconds || 0) : 0; const timeB = b.lastMessageAt ? (b.lastMessageAt.seconds || 0) : 0; return timeB - timeA; // Descending }); setDmConversations(enhancedConvos); setIsLoadingDms(false); }, (err) => { console.error("Error fetching DMs:", err); setError("Failed to load DMs."); setIsLoadingDms(false); }); return () => unsubscribe(); }, [db, userId, isUserNameSet]); // --- Fetch Messages (for selected channel OR DM) --- useEffect(() => { if (!db || !isUserNameSet || (!selectedChannelId && !selectedDmConversationId)) { setMessages([]); setIsLoadingMessages(false); return; } setIsLoadingMessages(true); let messagesPath; if (chatMode === 'channel' && selectedChannelId) { messagesPath = getGroupChannelMessagesPath(selectedChannelId); } else if (chatMode === 'dm' && selectedDmConversationId) { messagesPath = getDmMessagesPath(selectedDmConversationId); } else { setIsLoadingMessages(false); return; } const q = query(collection(db, messagesPath), orderBy("timestamp", "asc")); const unsubscribe = onSnapshot(q, (snapshot) => { setMessages(snapshot.docs.map(d => ({ id: d.id, ...d.data() }))); setIsLoadingMessages(false); scrollToBottom(); }, (err) => { console.error(`Error fetching messages for ${messagesPath}:`, err); setError(`Failed to load messages.`); setIsLoadingMessages(false); }); return () => unsubscribe(); }, [db, selectedChannelId, selectedDmConversationId, chatMode, isUserNameSet]); // --- Actions --- const handleCreateChannel = async () => { if (newChannelName.trim() === '' || !db || !userId) { setError("Channel name/user/db issue."); return; } try { const addedDoc = await addDoc(collection(db, CHANNELS_PATH), { name: newChannelName.trim(), createdBy: userId, creatorName: userName, createdAt: serverTimestamp() }); setNewChannelName(''); // Select the new channel setSelectedChannelId(addedDoc.id); setSelectedChannelName(newChannelName.trim()); setChatMode('channel'); setSelectedDmConversationId(null); setSelectedDmPeer(null); setError(null); } catch (e) { console.error("Error creating channel:", e); setError("Failed to create channel."); } }; const generateDmConversationId = (uid1, uid2) => [uid1, uid2].sort().join('_'); const handleStartOrSelectDm = async (peerUser) => { if (!userId || !userName || !db || !peerUser || !peerUser.id || !peerUser.displayName) { setError("Cannot start DM: Missing user information."); return; } const dmId = generateDmConversationId(userId, peerUser.id); const dmRef = doc(db, DM_CONVERSATIONS_PATH, dmId); try { const dmSnap = await getDoc(dmRef); if (!dmSnap.exists()) { await setDoc(dmRef, { participants: [userId, peerUser.id], participantNames: { [userId]: userName, [peerUser.id]: peerUser.displayName }, createdAt: serverTimestamp(), lastMessageAt: serverTimestamp() // Initialize for sorting }); console.log("Created new DM conversation:", dmId); } setSelectedDmConversationId(dmId); setSelectedDmPeer({ id: peerUser.id, name: peerUser.displayName }); setChatMode('dm'); setSelectedChannelId(null); // Clear group channel selection setSelectedChannelName(''); setShowUserListModal(false); // Close modal if open setError(null); } catch (e) { console.error("Error starting/selecting DM:", e); setError("Failed to start DM conversation."); } }; const handleSendMessage = async () => { if (newMessage.trim() === '' || !db || !userId || !userName) { setError("Message empty or user/db issue."); return; } let messagePayload = { text: newMessage.trim(), userId: userId, userName: userName, timestamp: serverTimestamp() }; let targetPath; let conversationRefToUpdate; // For lastMessageAt if (chatMode === 'channel' && selectedChannelId) { targetPath = getGroupChannelMessagesPath(selectedChannelId); // Optionally update channel's last message time if needed for sorting channels (not implemented here) } else if (chatMode === 'dm' && selectedDmConversationId) { targetPath = getDmMessagesPath(selectedDmConversationId); conversationRefToUpdate = doc(db, DM_CONVERSATIONS_PATH, selectedDmConversationId); } else { setError("No active channel or DM selected."); return; } try { await addDoc(collection(db, targetPath), messagePayload); if (conversationRefToUpdate) { // Update lastMessageAt for DM conversation await setDoc(conversationRefToUpdate, { lastMessageAt: serverTimestamp() }, { merge: true }); } setNewMessage(''); setError(null); scrollToBottom(); } catch (e) { console.error("Error sending message:", e); setError("Failed to send message."); } }; const formatTimestamp = (firebaseTimestamp) => { if (!firebaseTimestamp) return 'Sending...'; let date; if (firebaseTimestamp instanceof Timestamp) date = firebaseTimestamp.toDate(); else if (firebaseTimestamp.seconds) date = new Date(firebaseTimestamp.seconds * 1000 + (firebaseTimestamp.nanoseconds || 0) / 1000000); else return 'Invalid date'; return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; // --- Render Logic --- if (!userId) return

Initializing Chat...

Attempting to sign you in...

{error &&

{error}

}
; if (!isUserNameSet) return

Set Display Name

setTempUserName(e.target.value)} placeholder="Enter display name" className="w-full p-3 mb-4 bg-gray-600 border border-gray-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" onKeyPress={(e) => e.key === 'Enter' && handleSetUserName()} />{error &&

{error}

}

User ID: {userId}

; let currentChatName = "Select a conversation"; if (chatMode === 'channel' && selectedChannelName) currentChatName = selectedChannelName; if (chatMode === 'dm' && selectedDmPeer) currentChatName = selectedDmPeer.name; let placeholderText = "Message"; if (chatMode === 'channel' && selectedChannelName) placeholderText = `Message #${selectedChannelName}`; if (chatMode === 'dm' && selectedDmPeer) placeholderText = `Message ${selectedDmPeer.name}`; return (
{/* Sidebar */}

Chat App

User: {userName} ({userId.substring(0,8)})

{/* Create Channel */}

CHANNELS

setNewChannelName(e.target.value)} placeholder="New channel name" className="w-full p-2 mb-2 bg-gray-700 border border-gray-600 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" />
{/* Channel List */}
{isLoadingChannels ?

Loading channels...

: channels.length === 0 ?

No channels yet.

: channels.map(channel => (
{ setSelectedChannelId(channel.id); setSelectedChannelName(channel.name); setChatMode('channel'); setSelectedDmConversationId(null); setSelectedDmPeer(null); }} className={`flex items-center p-2 rounded-md cursor-pointer hover:bg-gray-700 ${chatMode === 'channel' && selectedChannelId === channel.id ? 'bg-blue-600 font-semibold' : 'bg-gray-800'}`}> {channel.name}
))}
{/* Direct Messages */}

DIRECT MESSAGES

{/* Max height for DM list */} {isLoadingDms ?

Loading DMs...

: dmConversations.length === 0 ?

No DMs yet.

: dmConversations.map(dm => (
{ setSelectedDmConversationId(dm.id); setSelectedDmPeer({id: dm.peerId, name: dm.peerName}); setChatMode('dm'); setSelectedChannelId(null); setSelectedChannelName('');}} className={`flex items-center p-2 rounded-md cursor-pointer hover:bg-gray-700 ${chatMode === 'dm' && selectedDmConversationId === dm.id ? 'bg-blue-600 font-semibold' : 'bg-gray-800'}`}> {dm.peerName || "DM Conversation"}
))}
{/* Main Chat Area */}
{(selectedChannelId || selectedDmConversationId) ? ( <>

{chatMode === 'channel' ? : } {currentChatName}

{isLoadingMessages ?

Loading messages...

: messages.length === 0 ?

No messages yet.

: messages.map(msg => (
{msg.userId !== userId &&

{msg.userName || "User"}

}

{msg.text}

{formatTimestamp(msg.timestamp)}

))}
setNewMessage(e.target.value)} placeholder={placeholderText} className="flex-1 p-3 bg-transparent text-gray-100 placeholder-gray-400 focus:outline-none" onKeyPress={(e) => e.key === 'Enter' && !e.shiftKey && (handleSendMessage(), e.preventDefault())} />
) : (

{isLoadingChannels || isLoadingDms ? "Loading..." : "Select or create a conversation."}

)} {error &&
Error: {error}
}
{/* User List Modal for DMs */} {showUserListModal && (

Start a new Direct Message

{isLoadingUsers ?

Loading users...

: allUsers.length === 0 ?

No other users found.

: allUsers.map(user => (
handleStartOrSelectDm(user)} className="flex items-center p-3 hover:bg-gray-700 rounded-md cursor-pointer"> {user.displayName} ({user.id.substring(0,8)})
))}
)}
); }; export default App;