const firebaseConfig = { apiKey: "AIzaSyC2jdXkwJGqHHmJDWzm1aVZL75OujQvUvI", authDomain: "computero-213b5.firebaseapp.com", databaseURL: "https://computero-213b5-default-rtdb.firebaseio.com", projectId: "computero-213b5", storageBucket: "computero-213b5.appspot.com", messagingSenderId: "300084057509", appId: "1:300084057509:web:cc9d92f5f70a92b7b4044c" }; firebase.initializeApp(firebaseConfig); // GIF API Key const GIPHY_API_KEY = 'EyJHZFDCFSFSE5g7VXSAsKBzDCDBqKHr'; let gifPickerVisible = false; let emojiPickerVisible = false; // Call related variables let localStream; let remoteStream; let peerConnection; let callTimer; let callStartTime; let currentCallType = null; // 'voice' or 'video' let isCaller = false; let callChannel; let callRequestRef; let callStatusRef; let currentCallId = null; let incomingCallListener; let currentIncomingCallId = null; let iceCandidatesRef; let answerRef; let offerRef; // Group chat variables let selectedGroupId = null; let currentGroupAvatar = null; // Heartbeat interval for status updates let heartbeatInterval; // Flag to track the unread message separator let unreadSeparatorAdded = false; function initUserProfile(user) { const userNameElement = document.getElementById('user-profile-name'); const sanitizedEmail = sanitizeEmail(user.email); // Load profile name from Firebase firebase.database().ref(`usernames/${sanitizedEmail}`).on('value', snapshot => { const username = snapshot.val(); userNameElement.textContent = username || user.email.split('@')[0].replace(/\./g, ' '); }); // Load profile photo from Firebase Database firebase.database().ref(`profile_photos/${sanitizedEmail}`).on('value', snapshot => { const photoData = snapshot.val(); if (photoData) { document.getElementById('user-profile-photo').src = photoData; } else { document.getElementById('user-profile-photo').src = 'profile.png'; } }); // Load dark mode preference loadDarkModePreference(); // Set up profile photo upload document.getElementById('profile-photo-upload').addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; if (!file.type.match('image.*')) { alert('Please select an image file'); return; } if (file.size > 5 * 1024 * 1024) { alert('File size should be less than 5MB'); return; } // Compress and upload the image compressImage(file, { quality: 0.7, maxWidth: 400, maxHeight: 400, success: (compressedFile) => { uploadProfilePhotoToDatabase(compressedFile, user); }, error: (err) => { console.error('Error compressing image:', err); alert('Error processing image'); } }); }); } function uploadProfilePhotoToDatabase(file, user) { const reader = new FileReader(); const currentUser = firebase.auth().currentUser; reader.onload = function(event) { const base64String = event.target.result; const sanitizedEmail = sanitizeEmail(user.email); // Save to Firebase Database under user's own reference firebase.database().ref(`profile_photos/${sanitizedEmail}`).set(base64String) .then(() => { // Update local profile photo document.getElementById('user-profile-photo').src = base64String; // Update profile photo in all friends' views updateProfilePhotoForAllFriends(currentUser.uid, base64String); // If we're in a chat with someone, update our avatar in the chat if (selectedUser || selectedGroupId) { const messages = document.querySelectorAll('.message.sender'); messages.forEach(message => { const avatar = message.parentElement.querySelector('img'); if (avatar) { avatar.src = base64String; } }); } }) .catch(error => { console.error('Error saving profile photo:', error); alert('Failed to save profile photo'); }); }; reader.onerror = function() { console.error('Error reading file'); alert('Error reading image file'); }; reader.readAsDataURL(file); } function updateProfilePhotoForAllFriends(userId, photoData) { const friendsRef = firebase.database().ref(`friends/${userId}`); friendsRef.once('value').then(snapshot => { snapshot.forEach(childSnapshot => { const friendEmail = childSnapshot.val(); const sanitizedFriendEmail = sanitizeEmail(friendEmail); // Update the photo reference for each friend firebase.database().ref(`profile_photos/${sanitizedFriendEmail}_contacts/${sanitizeEmail(currentUser.email)}`).set(photoData); }); }); } // Image preview function function showImagePreview(imageSrc) { const previewContainer = document.getElementById('image-preview-container'); const previewImage = document.getElementById('image-preview'); previewImage.src = imageSrc; previewContainer.style.display = 'flex'; } function closeImagePreview() { document.getElementById('image-preview-container').style.display = 'none'; } // Video preview functions function showVideoPreview(videoSrc) { const previewContainer = document.getElementById('video-preview-container'); const videoPlayer = document.getElementById('video-preview-player'); videoPlayer.src = videoSrc; previewContainer.style.display = 'flex'; } function closeVideoPreview() { const videoPlayer = document.getElementById('video-preview-player'); videoPlayer.pause(); videoPlayer.src = ''; document.getElementById('video-preview-container').style.display = 'none'; } // NEW: Centralized function to handle file uploads from both the input and drag-and-drop function handleFileUpload(file) { if (!file) return; // Check if a chat is selected if (!selectedUser && !selectedGroupId) { alert('Please select a chat to send a file.'); return; } // Check if file is an image or video if (!file.type.match('image.*') && !file.type.match('video.*')) { alert('Please select an image or video file'); return; } // Check file size (max 10MB for videos, 5MB for images) const maxSize = file.type.match('video.*') ? 10 : 5; if (file.size > maxSize * 1024 * 1024) { alert(`File size should be less than ${maxSize}MB`); return; } // Show upload progress const progressBar = document.getElementById('upload-progress-bar'); const progressContainer = document.getElementById('upload-progress'); progressContainer.style.display = 'block'; progressBar.style.width = '0%'; if (file.type.match('image.*')) { // Handle image upload compressImage(file, { quality: 0.7, maxWidth: 1024, maxHeight: 1024, success: (compressedFile) => { uploadMedia(compressedFile, 'image', progressBar, progressContainer); }, error: (err) => { console.error('Error compressing image:', err); alert('Error processing image'); progressContainer.style.display = 'none'; } }); } else if (file.type.match('video.*')) { // Handle video upload uploadMedia(file, 'video', progressBar, progressContainer); } } // UPDATED: File upload handling now uses the new centralized function document.getElementById('file-upload').addEventListener('change', function(e) { const file = e.target.files[0]; handleFileUpload(file); }); function compressImage(file, options) { const reader = new FileReader(); reader.onload = function(event) { const img = new Image(); img.src = event.target.result; img.onload = function() { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // Calculate new dimensions let width = img.width; let height = img.height; if (width > options.maxWidth) { height *= options.maxWidth / width; width = options.maxWidth; } if (height > options.maxHeight) { width *= options.maxHeight / height; height = options.maxHeight; } // Draw image on canvas canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); // Convert to blob with specified quality canvas.toBlob(function(blob) { const compressedFile = new File([blob], file.name, { type: 'image/jpeg', lastModified: Date.now() }); options.success(compressedFile); }, 'image/jpeg', options.quality); }; img.onerror = function() { options.error('Image load error'); }; }; reader.onerror = function() { options.error('File read error'); }; reader.readAsDataURL(file); } function uploadMedia(file, type, progressBar, progressContainer) { const reader = new FileReader(); reader.onload = function(event) { const base64String = event.target.result.split(',')[1]; const currentUser = firebase.auth().currentUser; if (!currentUser || (!selectedUser && !selectedGroupId)) { alert('You must be signed in and select a user/group to send files'); progressContainer.style.display = 'none'; return; } const currentUserId = sanitizeEmail(currentUser.email); // Create message data with actual media content const messageData = { sender: currentUser.email, timestamp: Date.now(), fileType: type, fileName: file.name }; if (type === 'image') { messageData.image = base64String; } else if (type === 'video') { messageData.video = base64String; } if (selectedUser) { // Direct message messageData.receiver = selectedUser; const sanitizedUser = sanitizeEmail(selectedUser); // Simulate upload progress let progress = 0; const progressInterval = setInterval(() => { progress += 5; if (progress >= 100) { clearInterval(progressInterval); progressBar.style.width = '100%'; setTimeout(() => { const senderRef = firebase.database().ref(`messages/${currentUserId}/${sanitizedUser}`).push(); const receiverRef = firebase.database().ref(`messages/${sanitizedUser}/${currentUserId}`).push(); senderRef.set(messageData); receiverRef.set(messageData); if (selectedUser !== currentUser.email) { const unreadRef = firebase.database().ref(`unreadMessages/${sanitizedUser}/${currentUserId}`); unreadRef.transaction((currentCount) => (currentCount || 0) + 1); } progressContainer.style.display = 'none'; document.getElementById('file-upload').value = ''; setTimeout(scrollToBottom); // MODIFIED: Added setTimeout }, 300); } else { progressBar.style.width = progress + '%'; } }, 50); } else if (selectedGroupId) { // Group message messageData.groupId = selectedGroupId; // Simulate upload progress let progress = 0; const progressInterval = setInterval(() => { progress += 5; if (progress >= 100) { clearInterval(progressInterval); progressBar.style.width = '100%'; setTimeout(() => { // Save message to group firebase.database().ref(`groupMessages/${selectedGroupId}`).push(messageData); // Update last message timestamp for group firebase.database().ref(`groups/${selectedGroupId}/lastMessageTimestamp`).set(Date.now()); // Update unread count for all group members except sender firebase.database().ref(`groups/${selectedGroupId}/members`).once('value').then(snapshot => { snapshot.forEach(memberSnapshot => { const memberEmail = memberSnapshot.val(); if (memberEmail !== currentUser.email) { const sanitizedMember = sanitizeEmail(memberEmail); firebase.database().ref(`unreadGroupMessages/${sanitizedMember}/${selectedGroupId}`).transaction((currentCount) => (currentCount || 0) + 1); } }); }); progressContainer.style.display = 'none'; document.getElementById('file-upload').value = ''; setTimeout(scrollToBottom); // MODIFIED: Added setTimeout }, 300); } else { progressBar.style.width = progress + '%'; } }, 50); } }; reader.onerror = function() { alert('Error reading file'); progressContainer.style.display = 'none'; }; reader.readAsDataURL(file); } function cleanupCallResources() { console.log('Cleaning up call resources...'); // Stop all media tracks if (localStream) { localStream.getTracks().forEach(track => { track.stop(); track.enabled = false; }); localStream = null; } if (remoteStream) { remoteStream.getTracks().forEach(track => { track.stop(); track.enabled = false; }); remoteStream = null; } // Close peer connection if (peerConnection) { // Remove all event listeners first peerConnection.onicecandidate = null; peerConnection.ontrack = null; peerConnection.oniceconnectionstatechange = null; peerConnection.onicegatheringstatechange = null; peerConnection.onsignalingstatechange = null; peerConnection.onnegotiationneeded = null; peerConnection.onconnectionstatechange = null; // Close the connection peerConnection.close(); peerConnection = null; } // Clear the call timer if (callTimer) { clearInterval(callTimer); callTimer = null; } // Remove all Firebase listeners if (callStatusRef) { callStatusRef.off(); callStatusRef = null; } if (iceCandidatesRef) { iceCandidatesRef.off(); iceCandidatesRef = null; } if (answerRef) { answerRef.off(); answerRef = null; } if (offerRef) { offerRef.off(); offerRef = null; } // Reset video elements const localVideo = document.getElementById('local-video'); const remoteVideo = document.getElementById('remote-video'); if (localVideo) { localVideo.srcObject = null; localVideo.pause(); } if (remoteVideo) { remoteVideo.srcObject = null; remoteVideo.pause(); } // Reset call data currentCallType = null; isCaller = false; callChannel = null; callRequestRef = null; currentCallId = null; } function addFriend() { const friendEmail = prompt("Enter your friend's email:"); if (!friendEmail) return alert("Friend email cannot be empty."); const currentUser = firebase.auth().currentUser; if (!currentUser) return alert("You must be signed in to add friends."); const friendsRef = firebase.database().ref(`friends/${currentUser.uid}`); friendsRef.push(friendEmail).then(() => { loadFriends(); }).catch(error => alert("Error: " + error.message)); } // 🚀 Optimized loadFriends() with delayed last message and group loading function loadFriends() { const currentUser = firebase.auth().currentUser; if (!currentUser) return; const friendsRef = firebase.database().ref(`friends/${currentUser.uid}`); const groupsRef = firebase.database().ref(`groupMembers/${sanitizeEmail(currentUser.email)}`); const usersContainer = document.getElementById('users'); usersContainer.innerHTML = ''; const friendsData = {}; const groupsData = {}; // 🧑 Load Friends friendsRef.on('value', snapshot => { snapshot.forEach(childSnapshot => { const friendEmail = childSnapshot.val(); const sanitizedUser = sanitizeEmail(friendEmail); const currentUserId = sanitizeEmail(currentUser.email); if (!friendsData[friendEmail]) { friendsData[friendEmail] = { email: friendEmail, sanitizedUser, currentUserId, lastMessage: '', lastTimestamp: 0, unreadCount: 0, button: null }; const button = document.createElement('button'); button.classList.add('friend-button'); button.setAttribute('data-email', friendEmail); button.onclick = () => startChat(friendEmail); const profileLogoContainer = document.createElement('div'); profileLogoContainer.classList.add('profile-logo-container'); const profileLogo = document.createElement('img'); profileLogo.src = 'profile.png'; profileLogo.classList.add('profile-logo'); profileLogoContainer.appendChild(profileLogo); const statusDot = document.createElement('div'); statusDot.classList.add('status-dot'); profileLogoContainer.appendChild(statusDot); button.appendChild(profileLogoContainer); const friendText = document.createElement('div'); friendText.classList.add('friend-text'); const nameSpan = document.createElement('span'); nameSpan.classList.add('friend-name'); nameSpan.textContent = friendEmail.split('@')[0].replace(/\./g, ' '); friendText.appendChild(nameSpan); firebase.database().ref(`usernames/${sanitizedUser}`).on('value', snap => { nameSpan.textContent = snap.exists() ? snap.val() : friendEmail.split('@')[0].replace(/\./g, ' '); }); const lastMessageSpan = document.createElement('span'); lastMessageSpan.classList.add('last-message'); friendText.appendChild(lastMessageSpan); button.appendChild(friendText); friendsData[friendEmail].button = button; usersContainer.appendChild(button); // Load profile photo firebase.database().ref(`profile_photos/${sanitizedUser}`).on('value', photoSnap => { if (photoSnap.exists()) { profileLogo.src = photoSnap.val(); } else { firebase.database().ref(`profile_photos/${currentUserId}_contacts/${sanitizedUser}`).on('value', contactPhotoSnap => { profileLogo.src = contactPhotoSnap.exists() ? contactPhotoSnap.val() : 'profile.png'; }); } }); // ✅ Delay loading last message setTimeout(() => { const messagesRef = firebase.database().ref(`messages/${currentUserId}/${sanitizedUser}`); messagesRef.orderByChild('timestamp').limitToLast(1).once('value').then(snap => { const lastMessage = Object.values(snap.val() || {})[0]; if (!lastMessage) return; let messageText = lastMessage.gif ? '🎁 GIF' : lastMessage.image ? '📷 Image' : lastMessage.video ? '🎥 Video' : lastMessage.text; friendsData[friendEmail].lastMessage = messageText; friendsData[friendEmail].lastTimestamp = lastMessage.timestamp; const lastMessageSpan = friendsData[friendEmail].button.querySelector('.last-message'); lastMessageSpan.textContent = messageText; sortAndDisplayFriends(friendsData, groupsData, usersContainer); }); }, 300); // Unread count firebase.database().ref(`unreadMessages/${currentUserId}/${sanitizedUser}`).on('value', unreadSnap => { const count = unreadSnap.val() || 0; friendsData[friendEmail].unreadCount = count; const unreadDot = friendsData[friendEmail].button.querySelector('.unread-dot'); const shouldShow = count > 0 && selectedUser !== friendEmail; if (shouldShow) { if (!unreadDot) { const dot = document.createElement('span'); dot.classList.add('unread-dot'); dot.textContent = count; friendsData[friendEmail].button.appendChild(dot); } else { unreadDot.textContent = count; } } else if (unreadDot) { unreadDot.remove(); } }); const statusRef = firebase.database().ref(`status/${sanitizedUser}`); const statusUpdatedRef = firebase.database().ref(`statusLastUpdated/${sanitizedUser}`); function updateStatusDot() { Promise.all([ statusRef.once('value'), statusUpdatedRef.once('value') ]).then(([statusSnap, tsSnap]) => { const isOnline = statusSnap.val() === 'online' && (Date.now() - tsSnap.val()) < 30000; const dot = friendsData[friendEmail].button.querySelector('.status-dot'); if (dot) { dot.classList.toggle('online', isOnline); dot.classList.toggle('offline', !isOnline); } }); } // Listen for changes to both status and timestamp statusRef.on('value', updateStatusDot); statusUpdatedRef.on('value', updateStatusDot); } }); }); // 👥 Load Groups (Optimized) groupsRef.on('value', snapshot => { snapshot.forEach(childSnapshot => { const groupId = childSnapshot.key; firebase.database().ref(`groups/${groupId}`).once('value').then(groupSnap => { const groupData = groupSnap.val(); if (!groupsData[groupId]) { groupsData[groupId] = { id: groupId, name: groupData.name, avatar: groupData.avatar, lastMessage: '', lastTimestamp: groupData.lastMessageTimestamp || 0, unreadCount: 0, button: null }; const button = document.createElement('button'); button.classList.add('friend-button'); button.setAttribute('data-group-id', groupId); button.onclick = () => startGroupChat(groupId); const groupLogoContainer = document.createElement('div'); groupLogoContainer.classList.add('profile-logo-container'); const groupLogo = document.createElement('div'); groupLogo.classList.add('group-logo'); if (groupData.avatar) { groupLogo.style.backgroundImage = `url(${groupData.avatar})`; groupLogo.style.backgroundSize = 'cover'; groupLogo.style.backgroundPosition = 'center'; } else { const initials = groupData.name.split(' ').map(w => w[0]).join('').substring(0, 2); groupLogo.textContent = initials.toUpperCase(); } groupLogoContainer.appendChild(groupLogo); button.appendChild(groupLogoContainer); const groupText = document.createElement('div'); groupText.classList.add('friend-text'); const nameSpan = document.createElement('span'); nameSpan.classList.add('friend-name'); nameSpan.textContent = groupData.name; const lastMessageSpan = document.createElement('span'); lastMessageSpan.classList.add('last-message'); groupText.appendChild(nameSpan); groupText.appendChild(lastMessageSpan); button.appendChild(groupText); groupsData[groupId].button = button; usersContainer.appendChild(button); // ✅ Delay message fetch setTimeout(() => { firebase.database().ref(`groupMessages/${groupId}`).orderByChild('timestamp').limitToLast(1).once('child_added').then(msgSnap => { if (!msgSnap.exists()) return; const msg = msgSnap.val(); const text = msg.gif ? '🎁 GIF' : msg.image ? '📷 Image' : msg.video ? '🎥 Video' : msg.text; groupsData[groupId].lastMessage = text; groupsData[groupId].lastTimestamp = msg.timestamp; const msgSpan = groupsData[groupId].button.querySelector('.last-message'); msgSpan.textContent = text; sortAndDisplayFriends(friendsData, groupsData, usersContainer); }); }, 300); // Unread const unreadRef = firebase.database().ref(`unreadGroupMessages/${sanitizeEmail(currentUser.email)}/${groupId}`); unreadRef.on('value', unreadSnap => { const count = unreadSnap.val() || 0; groupsData[groupId].unreadCount = count; const unreadDot = groupsData[groupId].button.querySelector('.unread-dot'); const shouldShow = count > 0 && selectedGroupId !== groupId; if (shouldShow) { if (!unreadDot) { const dot = document.createElement('span'); dot.classList.add('unread-dot'); dot.textContent = count; groupsData[groupId].button.appendChild(dot); } else { unreadDot.textContent = count; } } else if (unreadDot) { unreadDot.remove(); } }); } }); }); }); } function sortAndDisplayFriends(friendsData, groupsData, usersContainer) { // Combine friends and groups into one array const allChats = [ ...Object.values(friendsData), ...Object.values(groupsData) ]; // Sort by last message timestamp (newest first) const sortedChats = allChats.sort((a, b) => b.lastTimestamp - a.lastTimestamp); // Clear the container and re-add sorted chats usersContainer.innerHTML = ''; sortedChats.forEach(chat => { usersContainer.appendChild(chat.button); // Ensure the button's last message is up to date const lastMessageSpan = chat.button.querySelector('.last-message'); if (lastMessageSpan) { lastMessageSpan.textContent = chat.lastMessage; } }); } let selectedUser = null; function listenForTypingStatus() { const currentUser = firebase.auth().currentUser; if (!currentUser || (!selectedUser && !selectedGroupId)) return; const currentUserId = sanitizeEmail(currentUser.email); if (window.currentTypingStatusRef) { window.currentTypingStatusRef.off(); } const typingIndicator = document.getElementById('typing-indicator'); if (selectedUser) { // ✅ Listen for the OTHER user's typing status about YOU window.currentTypingStatusRef = firebase.database().ref(`typingStatus/${sanitizeEmail(selectedUser)}/${currentUserId}`); window.currentTypingStatusRef.on('value', async (snapshot) => { const isTyping = snapshot.val(); // ✅ Only show if the OTHER user is typing (not you) if (isTyping) { const name = await formatName(selectedUser); typingIndicator.textContent = `${name} is typing...`; } else { typingIndicator.textContent = ''; } }); } else if (selectedGroupId) { // Group chat logic window.currentTypingStatusRef = firebase.database().ref(`typingStatusGroup/${selectedGroupId}`); window.currentTypingStatusRef.on('value', async (snapshot) => { if (!snapshot.exists()) { typingIndicator.textContent = ''; return; } const typingUsers = []; snapshot.forEach(userSnapshot => { const userId = userSnapshot.key; const isTyping = userSnapshot.val(); if (isTyping && userId !== currentUserId) { typingUsers.push(userId); } }); if (typingUsers.length === 0) { typingIndicator.textContent = ''; return; } const names = []; for (const userId of typingUsers) { const name = await formatName(userId.replace(/_/g, '.')); names.push(name); } if (names.length === 1) { typingIndicator.textContent = `${names[0]} is typing...`; } else if (names.length === 2) { typingIndicator.textContent = `${names[0]} and ${names[1]} are typing...`; } else { typingIndicator.textContent = `${names[0]}, ${names[1]} and others are typing...`; } }); } } function startChat(friendEmail) { // Reset group selection selectedGroupId = null; // Remove active class from all buttons document.querySelectorAll('.friend-button').forEach(btn => { btn.classList.remove('active'); }); // Add active class to selected button const friendButton = document.querySelector(`button[data-email="${friendEmail}"]`); if (friendButton) { friendButton.classList.add('active'); } selectedUser = friendEmail; const chatBox = document.getElementById("chat"); const messageInput = document.getElementById("message"); if (selectedUser) { // Create chat header with call buttons const chatHeader = document.getElementById('chat-header'); chatHeader.innerHTML = ''; // Add profile photo to chat header const profilePhoto = document.createElement('img'); profilePhoto.style.width = '32px'; profilePhoto.style.height = '32px'; profilePhoto.style.borderRadius = '50%'; profilePhoto.style.marginRight = '8px'; // Load profile photo const sanitizedUser = sanitizeEmail(selectedUser); firebase.database().ref(`profile_photos/${sanitizedUser}`).once('value').then(snapshot => { profilePhoto.src = snapshot.exists() ? snapshot.val() : 'profile.png'; }); chatHeader.appendChild(profilePhoto); // Add name element (will be updated from Firebase) const nameElement = document.createElement('h2'); nameElement.textContent = selectedUser.split('@')[0].replace(/\./g, ' '); // Default to email prefix chatHeader.appendChild(nameElement); // Add listener for username changes in Firebase firebase.database().ref(`usernames/${sanitizedUser}`).on('value', snapshot => { if (snapshot.exists()) { nameElement.textContent = snapshot.val(); // Update with custom name if available } else { nameElement.textContent = selectedUser.split('@')[0].replace(/\./g, ' '); // Fallback to email prefix } }); // Add call buttons const headerActions = document.createElement('div'); headerActions.id = 'chat-header-actions'; const voiceCallBtn = document.createElement('button'); voiceCallBtn.className = 'call-header-btn'; voiceCallBtn.id = 'voice-call-btn'; voiceCallBtn.title = 'Voice Call'; voiceCallBtn.innerHTML = ''; voiceCallBtn.addEventListener('click', () => initiateCall('voice')); const videoCallBtn = document.createElement('button'); videoCallBtn.className = 'call-header-btn'; videoCallBtn.id = 'video-call-btn'; videoCallBtn.title = 'Video Call'; videoCallBtn.innerHTML = ''; videoCallBtn.addEventListener('click', () => initiateCall('video')); headerActions.appendChild(voiceCallBtn); headerActions.appendChild(videoCallBtn); chatHeader.appendChild(headerActions); chatBox.style.display = "flex"; messageInput.style.display = "block"; // This single call now handles loading, scrolling, and marking as read. loadMessages(); // Start listening for typing status listenForTypingStatus(); } } function startGroupChat(groupId) { // Reset direct message selection selectedUser = null; // Remove active class from all buttons document.querySelectorAll('.friend-button').forEach(btn => { btn.classList.remove('active'); }); // Add active class to selected group button const groupButton = document.querySelector(`button[data-group-id="${groupId}"]`); if (groupButton) { groupButton.classList.add('active'); } selectedGroupId = groupId; const chatBox = document.getElementById("chat"); const messageInput = document.getElementById("message"); if (selectedGroupId) { // Create group chat header const chatHeader = document.getElementById('chat-header'); chatHeader.innerHTML = ''; // Get group info firebase.database().ref(`groups/${selectedGroupId}`).once('value').then(snapshot => { const groupData = snapshot.val(); // Add group avatar to header const groupAvatar = document.createElement('div'); groupAvatar.style.width = '32px'; groupAvatar.style.height = '32px'; groupAvatar.style.borderRadius = '50%'; groupAvatar.style.marginRight = '8px'; groupAvatar.style.display = 'flex'; groupAvatar.style.alignItems = 'center'; groupAvatar.style.justifyContent = 'center'; groupAvatar.style.fontWeight = 'bold'; groupAvatar.style.backgroundColor = 'var(--group-chat)'; groupAvatar.style.color = 'white'; if (groupData.avatar) { groupAvatar.style.backgroundImage = `url(${groupData.avatar})`; groupAvatar.style.backgroundSize = 'cover'; groupAvatar.style.backgroundPosition = 'center'; } else { // Use initials if no avatar const initials = groupData.name.split(' ').map(word => word[0]).join('').substring(0, 2); groupAvatar.textContent = initials.toUpperCase(); } // Add click handler to show group info groupAvatar.style.cursor = 'pointer'; groupAvatar.addEventListener('click', () => showGroupInfoModal(selectedGroupId)); chatHeader.appendChild(groupAvatar); // Add group name const nameElement = document.createElement('h2'); nameElement.textContent = groupData.name; nameElement.style.cursor = 'pointer'; nameElement.addEventListener('click', () => showGroupInfoModal(selectedGroupId)); chatHeader.appendChild(nameElement); // Add group info button const infoBtn = document.createElement('button'); infoBtn.className = 'call-header-btn'; infoBtn.title = 'Group Info'; infoBtn.innerHTML = ''; infoBtn.addEventListener('click', () => showGroupInfoModal(selectedGroupId)); const headerActions = document.createElement('div'); headerActions.id = 'chat-header-actions'; headerActions.appendChild(infoBtn); chatHeader.appendChild(headerActions); }); chatBox.style.display = "flex"; messageInput.style.display = "block"; // This single call now handles loading, scrolling, and marking as read. loadGroupMessages(); // Start listening for typing status listenForTypingStatus(); } } async function loadMessages() { if (!selectedUser) return; const currentUser = firebase.auth().currentUser; if (!currentUser) return; const currentUserId = sanitizeEmail(currentUser.email); const sanitizedUser = sanitizeEmail(selectedUser); const messagesRef = firebase.database().ref(`messages/${currentUserId}/${sanitizedUser}`); const unreadRef = firebase.database().ref(`unreadMessages/${currentUserId}/${sanitizedUser}`); // Detach all old listeners to prevent duplication messagesRef.off(); const messagesDiv = document.getElementById('messages'); messagesDiv.innerHTML = ''; lastDisplayedDate = null; unreadSeparatorAdded = false; // FIX #1: Get unread count and immediately reset it to 0 when opening the chat. const unreadSnapshot = await unreadRef.once('value'); const unreadCount = unreadSnapshot.val() || 0; if (unreadCount > 0) { unreadRef.set(0); } // Then, fetch all messages for the chat. const messagesSnapshot = await messagesRef.orderByChild('timestamp').once('value'); let unreadSeparator = null; if (messagesSnapshot.exists()) { const allMessages = []; messagesSnapshot.forEach(child => { allMessages.push({key: child.key, ...child.val()}); }); const totalMessages = allMessages.length; // Iterate and display all messages allMessages.forEach((message, index) => { if (unreadCount > 0 && !unreadSeparatorAdded && index === totalMessages - unreadCount) { unreadSeparator = document.createElement('div'); unreadSeparator.classList.add('unread-separator'); unreadSeparator.innerHTML = 'Unread'; messagesDiv.appendChild(unreadSeparator); unreadSeparatorAdded = true; } // Display the message itself const isSender = message.sender === currentUser.email; displayMessage(message, isSender, message.timestamp); }); } // Scroll to the separator if it was added, otherwise to the bottom. if (unreadSeparator) { unreadSeparator.scrollIntoView({ behavior: 'auto', block: 'center' }); } else { setTimeout(scrollToBottom); // MODIFIED: Added setTimeout for initial load } // Now, attach a listener for *new* messages that arrive after this initial load. messagesRef.orderByChild('timestamp').startAt(Date.now()).on('child_added', snapshot => { const message = snapshot.val(); if ((message.sender === currentUser.email && message.receiver === selectedUser) || (message.sender === selectedUser && message.receiver === currentUser.email)) { // FIX #2: When a new message arrives in the active chat, reset the count again. if (message.sender === selectedUser) { unreadRef.set(0); } if (document.querySelector('.unread-separator')) { document.querySelector('.unread-separator').remove(); } const isSender = message.sender === currentUser.email; displayMessage(message, isSender, message.timestamp); setTimeout(scrollToBottom); // MODIFIED: Added setTimeout for new messages } }); } async function loadGroupMessages() { if (!selectedGroupId) return; const currentUser = firebase.auth().currentUser; if (!currentUser) return; const currentUserId = sanitizeEmail(currentUser.email); const messagesRef = firebase.database().ref(`groupMessages/${selectedGroupId}`); const unreadRef = firebase.database().ref(`unreadGroupMessages/${currentUserId}/${selectedGroupId}`); messagesRef.off(); const messagesDiv = document.getElementById('messages'); messagesDiv.innerHTML = ''; lastDisplayedDate = null; unreadSeparatorAdded = false; // FIX #1: Get unread count and immediately reset it to 0 for the group. const unreadSnapshot = await unreadRef.once('value'); const unreadCount = unreadSnapshot.val() || 0; if (unreadCount > 0) { unreadRef.set(0); } // Fetch all messages const messagesSnapshot = await messagesRef.orderByChild('timestamp').once('value'); let unreadSeparator = null; if (messagesSnapshot.exists()) { const allMessages = []; messagesSnapshot.forEach(child => { allMessages.push(child.val()); }); const totalMessages = allMessages.length; // Display messages and separator allMessages.forEach((message, index) => { if (unreadCount > 0 && !unreadSeparatorAdded && index === totalMessages - unreadCount) { unreadSeparator = document.createElement('div'); unreadSeparator.classList.add('unread-separator'); unreadSeparator.innerHTML = 'Unread'; messagesDiv.appendChild(unreadSeparator); unreadSeparatorAdded = true; } if (message.groupId === selectedGroupId) { const isSender = message.sender === currentUser.email; displayGroupMessage(message, isSender, message.timestamp); } }); } // Scroll to position if (unreadSeparator) { unreadSeparator.scrollIntoView({ behavior: 'auto', block: 'center' }); } else { setTimeout(scrollToBottom); // MODIFIED: Added setTimeout for initial load } // Listen for new messages messagesRef.orderByChild('timestamp').startAt(Date.now()).on('child_added', snapshot => { const message = snapshot.val(); if (message.groupId === selectedGroupId) { // FIX #2: When a new message arrives in the active group, reset the count. if (message.sender !== currentUser.email) { unreadRef.set(0); } if (document.querySelector('.unread-separator')) { document.querySelector('.unread-separator').remove(); } const isSender = message.sender === currentUser.email; displayGroupMessage(message, isSender, message.timestamp); updateSidebarGroupMessage(selectedGroupId, message.text || (message.image ? '📷 Image' : '🎥 Video'), message.timestamp); setTimeout(scrollToBottom); // MODIFIED: Added setTimeout for new messages } }); } function scrollToBottom() { const messagesDiv = document.getElementById('messages'); messagesDiv.scrollTop = messagesDiv.scrollHeight; } function sanitizeEmail(email) { return email.replace(/\./g, '_'); } function sendMessage() { const messageInput = document.getElementById('message'); let messageText = messageInput.value.trim(); if (!messageText && !selectedGroupId && !selectedUser) return; const currentUser = firebase.auth().currentUser; if (!currentUser) return alert("You must be signed in to send messages."); const currentUserId = sanitizeEmail(currentUser.email); // Update user's online status and timestamp when sending a message firebase.database().ref(`status/${currentUserId}`).set('online'); firebase.database().ref(`statusLastUpdated/${currentUserId}`).set(Date.now()); if (selectedUser) { // Direct message const sanitizedUser = sanitizeEmail(selectedUser); const messageData = { sender: currentUser.email, receiver: selectedUser, text: messageText.replace(/\n/g, '
'), // Replace newlines with
tags timestamp: Date.now() }; const senderRef = firebase.database().ref(`messages/${currentUserId}/${sanitizedUser}`).push(); const receiverRef = firebase.database().ref(`messages/${sanitizedUser}/${currentUserId}`).push(); senderRef.set(messageData); receiverRef.set(messageData); if (selectedUser !== currentUser.email) { const unreadRef = firebase.database().ref(`unreadMessages/${sanitizedUser}/${currentUserId}`); unreadRef.transaction((currentCount) => (currentCount || 0) + 1); } // Update sidebar with first line only, no
tags updateSidebarMessage(selectedUser, messageText.split('\n')[0], Date.now()); } else if (selectedGroupId) { // Group message const messageData = { sender: currentUser.email, text: messageText.replace(/\n/g, '
'), // Replace newlines with
tags timestamp: Date.now(), groupId: selectedGroupId }; // Save message to group firebase.database().ref(`groupMessages/${selectedGroupId}`).push(messageData); // Update last message timestamp for group firebase.database().ref(`groups/${selectedGroupId}/lastMessageTimestamp`).set(Date.now()); // Update unread count for all group members except sender firebase.database().ref(`groups/${selectedGroupId}/members`).once('value').then(snapshot => { snapshot.forEach(memberSnapshot => { const memberEmail = memberSnapshot.val(); if (memberEmail !== currentUser.email) { const sanitizedMember = sanitizeEmail(memberEmail); firebase.database().ref(`unreadGroupMessages/${sanitizedMember}/${selectedGroupId}`).transaction((currentCount) => (currentCount || 0) + 1); } }); }); // Update sidebar with first line only, no
tags updateSidebarGroupMessage(selectedGroupId, messageText.split('\n')[0], Date.now()); } messageInput.value = ""; // Reset textarea height after sending messageInput.style.height = 'auto'; messageInput.style.overflowY = 'hidden'; updateTypingStatus(false); } function sendGif(gifUrl) { if (!selectedUser && !selectedGroupId) return; const currentUser = firebase.auth().currentUser; if (!currentUser) return alert("You must be signed in to send messages."); const currentUserId = sanitizeEmail(currentUser.email); const messageData = { sender: currentUser.email, timestamp: Date.now(), gif: gifUrl }; if (selectedUser) { messageData.receiver = selectedUser; const sanitizedUser = sanitizeEmail(selectedUser); const senderRef = firebase.database().ref(`messages/${currentUserId}/${sanitizedUser}`).push(); const receiverRef = firebase.database().ref(`messages/${sanitizedUser}/${currentUserId}`).push(); senderRef.set(messageData); receiverRef.set(messageData); if (selectedUser !== currentUser.email) { const unreadRef = firebase.database().ref(`unreadMessages/${sanitizedUser}/${currentUserId}`); unreadRef.transaction((currentCount) => (currentCount || 0) + 1); } updateSidebarMessage(selectedUser, '🎁 GIF', Date.now()); setTimeout(scrollToBottom); // MODIFIED: Added setTimeout } else if (selectedGroupId) { messageData.groupId = selectedGroupId; firebase.database().ref(`groupMessages/${selectedGroupId}`).push(messageData); firebase.database().ref(`groups/${selectedGroupId}/lastMessageTimestamp`).set(Date.now()); firebase.database().ref(`groups/${selectedGroupId}/members`).once('value').then(snapshot => { snapshot.forEach(memberSnapshot => { const memberEmail = memberSnapshot.val(); if (memberEmail !== currentUser.email) { const sanitizedMember = sanitizeEmail(memberEmail); firebase.database().ref(`unreadGroupMessages/${sanitizedMember}/${selectedGroupId}`).transaction((currentCount) => (currentCount || 0) + 1); } }); }); updateSidebarGroupMessage(selectedGroupId, '🎁 GIF', Date.now()); setTimeout(scrollToBottom); // MODIFIED: Added setTimeout } } // REMOVED: Old keypress listener // document.getElementById('message').addEventListener('keypress', ...); function formatTimestamp(timestamp) { const date = new Date(timestamp); return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function formatDate(timestamp) { const date = new Date(timestamp); return date.toLocaleDateString([], { year: 'numeric', month: 'long', day: 'numeric' }); } function displayDaySeparator(date) { const messagesDiv = document.getElementById('messages'); const separator = document.createElement('div'); separator.classList.add('day-separator'); separator.textContent = date; messagesDiv.appendChild(separator); } let lastDisplayedDate = null; function isSingleEmoji(text) { if (!text) return false; return /^[\p{Extended_Pictographic}]$/u.test(text.trim()); } function displayMessage(message, isSender, timestamp) { const messagesDiv = document.getElementById('messages'); const messageDate = formatDate(timestamp); if (messageDate !== lastDisplayedDate) { displayDaySeparator(messageDate); lastDisplayedDate = messageDate; } // Create message container const messageContainer = document.createElement('div'); messageContainer.style.display = 'flex'; messageContainer.style.gap = '8px'; messageContainer.style.alignItems = 'flex-start'; messageContainer.style.marginBottom = '16px'; messageContainer.style.justifyContent = isSender ? 'flex-end' : 'flex-start'; if (!isSender) { const senderPhoto = document.createElement('img'); Object.assign(senderPhoto.style, { width: '32px', height: '32px', borderRadius: '50%', objectFit: 'cover', marginTop: '4px' }); firebase.database().ref(`profile_photos/${sanitizeEmail(message.sender)}`).once('value').then(snapshot => { senderPhoto.src = snapshot.exists() ? snapshot.val() : 'profile.png'; }); messageContainer.appendChild(senderPhoto); } const messageDiv = document.createElement('div'); messageDiv.classList.add('message', isSender ? 'sender' : 'receiver'); if (message.text && isSingleEmoji(message.text)) { messageDiv.classList.add('emoji-only'); messageDiv.textContent = message.text; const timestampDiv = document.createElement('div'); timestampDiv.classList.add('message-timestamp'); const date = new Date(message.timestamp); const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); timestampDiv.textContent = `${hours}:${minutes}`; messageDiv.appendChild(timestampDiv); messageContainer.appendChild(messageDiv); messagesDiv.appendChild(messageContainer); return; } if (message.image) { const img = document.createElement('img'); img.src = `data:image/jpeg;base64,${message.image}`; img.classList.add('message-image'); img.onclick = () => showImagePreview(img.src); messageDiv.appendChild(img); if (message.fileType === 'video') { const fileType = document.createElement('div'); fileType.classList.add('file-type-indicator'); fileType.textContent = 'Video thumbnail'; messageDiv.appendChild(fileType); } } else if (message.video) { // Handle video message (existing code) const videoContainer = document.createElement('div'); videoContainer.classList.add('message-video'); videoContainer.onclick = () => showVideoPreview(`data:video/mp4;base64,${message.video}`); const thumbnail = document.createElement('div'); Object.assign(thumbnail.style, { width: '100%', height: '200px', backgroundColor: '#333', display: 'flex', justifyContent: 'center', alignItems: 'center', color: 'white' }); thumbnail.textContent = 'Video Message'; videoContainer.appendChild(thumbnail); const playIcon = document.createElement('i'); playIcon.classList.add('fas', 'fa-play', 'video-play-icon'); videoContainer.appendChild(playIcon); messageDiv.appendChild(videoContainer); const fileType = document.createElement('div'); fileType.classList.add('file-type-indicator'); fileType.textContent = message.fileName || 'Click to play video'; messageDiv.appendChild(fileType); } else if (message.gif) { const gifContainer = document.createElement('div'); gifContainer.classList.add('message-content-gif'); const gifImg = document.createElement('img'); gifImg.src = message.gif; gifContainer.appendChild(gifImg); messageDiv.appendChild(gifContainer); } else { const textContainer = document.createElement('div'); // Replace
tags with actual line breaks const messageText = message.text.replace(/
/g, '\n'); // Check for URLs in the message (existing code) const urlRegex = /(https?:\/\/[^\s]+)/g; const urls = messageText.match(urlRegex); let textWithoutUrls = messageText; // Inside displayMessage function if (urls) { const previewContainer = document.createElement('div'); previewContainer.classList.add('link-preview-container'); urls.forEach(url => { textWithoutUrls = textWithoutUrls.replace(url, ''); const previewWrapper = document.createElement('a'); previewWrapper.href = url; previewWrapper.target = '_blank'; previewWrapper.classList.add('link-preview-wrapper'); previewWrapper.style.textDecoration = 'none'; const preview = document.createElement('div'); preview.classList.add('link-preview'); preview.innerHTML = ` `; // Add original link below preview const originalLink = document.createElement('div'); originalLink.classList.add('original-link'); originalLink.innerHTML = `${url}`; previewWrapper.appendChild(preview); previewWrapper.appendChild(originalLink); // Add original link below preview previewContainer.appendChild(previewWrapper); // Fetch link preview data fetchLinkPreview(url).then(previewData => { if (previewData) { preview.innerHTML = ` ${previewData.image ? `Preview` : ''} `; } }); }); textContainer.appendChild(previewContainer); } if (textWithoutUrls.trim()) { const lines = textWithoutUrls.split('\n'); lines.forEach((line, index) => { if (line.trim()) { const textElement = document.createElement('div'); textElement.textContent = line; textContainer.appendChild(textElement); } }); } messageDiv.appendChild(textContainer); } const timestampDiv = document.createElement('div'); timestampDiv.classList.add('message-timestamp'); timestampDiv.textContent = formatTimestamp(timestamp); messageDiv.appendChild(timestampDiv); messageContainer.appendChild(messageDiv); if (isSender) { const myPhoto = document.createElement('img'); Object.assign(myPhoto.style, { width: '32px', height: '32px', borderRadius: '50%', objectFit: 'cover', marginTop: '4px' }); const currentUser = firebase.auth().currentUser; if (currentUser) { firebase.database().ref(`profile_photos/${sanitizeEmail(currentUser.email)}`).once('value').then(snapshot => { myPhoto.src = snapshot.exists() ? snapshot.val() : 'profile.png'; }); } messageContainer.appendChild(myPhoto); } messagesDiv.appendChild(messageContainer); } // Add this new function to fetch link previews async function fetchLinkPreview(url) { try { // You'll need to use a link preview API service here // Example using a free service (you may need to sign up for an API key) const response = await fetch(`https://api.linkpreview.net/?key=48cda21c02c61b2e06a61202a642e2f8&q=${encodeURIComponent(url)}`); if (!response.ok) throw new Error('Failed to fetch preview'); const data = await response.json(); return { title: data.title, description: data.description, image: data.image, domain: data.url ? new URL(data.url).hostname : null }; } catch (error) { console.error('Error fetching link preview:', error); return null; } } function displayGroupMessage(message, isSender, timestamp) { const messagesDiv = document.getElementById('messages'); const messageDate = formatDate(timestamp); if (messageDate !== lastDisplayedDate) { displayDaySeparator(messageDate); lastDisplayedDate = messageDate; } const messageContainer = document.createElement('div'); messageContainer.style.display = 'flex'; messageContainer.style.gap = '8px'; messageContainer.style.alignItems = 'flex-start'; messageContainer.style.marginBottom = '16px'; messageContainer.dataset.timestamp = timestamp; messageContainer.style.justifyContent = isSender ? 'flex-end' : 'flex-start'; if (!isSender) { const senderPhoto = document.createElement('img'); Object.assign(senderPhoto.style, { width: '32px', height: '32px', borderRadius: '50%', objectFit: 'cover', marginTop: '4px' }); firebase.database().ref(`profile_photos/${sanitizeEmail(message.sender)}`).once('value').then(snapshot => { senderPhoto.src = snapshot.exists() ? snapshot.val() : 'profile.png'; }); messageContainer.appendChild(senderPhoto); } const messageContentDiv = document.createElement('div'); messageContentDiv.classList.add('message', isSender ? 'sender' : 'receiver'); if (selectedGroupId && !isSender) { const senderNameSpan = document.createElement('span'); senderNameSpan.classList.add('message-sender-name'); firebase.database().ref(`usernames/${sanitizeEmail(message.sender)}`).once('value').then(nameSnap => { const name = nameSnap.exists() ? nameSnap.val() : message.sender.split('@')[0].replace(/\./g, ' '); senderNameSpan.textContent = name; }); messageContentDiv.appendChild(senderNameSpan); } if (message.text && isSingleEmoji(message.text)) { messageContentDiv.classList.add('emoji-only'); const textElement = document.createElement('div'); textElement.textContent = message.text; messageContentDiv.appendChild(textElement); } else if (message.image) { const img = document.createElement('img'); img.src = `data:image/jpeg;base64,${message.image}`; img.classList.add('message-image'); img.onclick = () => showImagePreview(img.src); messageContentDiv.appendChild(img); if (message.fileType === 'video') { const fileType = document.createElement('div'); fileType.classList.add('file-type-indicator'); fileType.textContent = 'Video thumbnail'; messageContentDiv.appendChild(fileType); } } else if (message.video) { const videoContainer = document.createElement('div'); videoContainer.classList.add('message-video'); videoContainer.onclick = () => showVideoPreview(`data:video/mp4;base64,${message.video}`); const thumbnail = document.createElement('div'); Object.assign(thumbnail.style, { width: '100%', height: '200px', backgroundColor: '#333', display: 'flex', justifyContent: 'center', alignItems: 'center', color: 'white' }); thumbnail.textContent = 'Video Message'; videoContainer.appendChild(thumbnail); const playIcon = document.createElement('i'); playIcon.classList.add('fas', 'fa-play', 'video-play-icon'); videoContainer.appendChild(playIcon); messageContentDiv.appendChild(videoContainer); const fileType = document.createElement('div'); fileType.classList.add('file-type-indicator'); fileType.textContent = message.fileName || 'Click to play video'; messageContentDiv.appendChild(fileType); } else if (message.gif) { const gifContainer = document.createElement('div'); gifContainer.classList.add('message-content-gif'); const gifImg = document.createElement('img'); gifImg.src = message.gif; gifContainer.appendChild(gifImg); messageContentDiv.appendChild(gifContainer); }else { const textContainer = document.createElement('div'); // Replace
tags with actual line breaks const messageText = message.text.replace(/
/g, '\n'); // Check for URLs in the message (existing code) const urlRegex = /(https?:\/\/[^\s]+)/g; const urls = messageText.match(urlRegex); let textWithoutUrls = messageText; // Inside displayGroupMessage function // Inside displayGroupMessage function if (urls) { const previewContainer = document.createElement('div'); previewContainer.classList.add('link-preview-container'); urls.forEach(url => { textWithoutUrls = textWithoutUrls.replace(url, ''); const previewWrapper = document.createElement('a'); previewWrapper.href = url; previewWrapper.target = '_blank'; previewWrapper.classList.add('link-preview-wrapper'); previewWrapper.style.textDecoration = 'none'; const preview = document.createElement('div'); preview.classList.add('link-preview'); preview.innerHTML = ` `; // Add original link below preview const originalLink = document.createElement('div'); originalLink.classList.add('original-link'); originalLink.innerHTML = `${url}`; previewWrapper.appendChild(preview); previewWrapper.appendChild(originalLink); // Add original link below preview previewContainer.appendChild(previewWrapper); fetchLinkPreview(url).then(previewData => { if (previewData) { preview.innerHTML = ` ${previewData.image ? `Preview` : ''} `; } }); }); textContainer.appendChild(previewContainer); } if (textWithoutUrls.trim()) { const lines = textWithoutUrls.split('\n'); lines.forEach((line, index) => { if (line.trim()) { const textElement = document.createElement('div'); textElement.textContent = line; textContainer.appendChild(textElement); } }); } messageContentDiv.appendChild(textContainer); } const timestampDiv = document.createElement('div'); timestampDiv.classList.add('message-timestamp'); timestampDiv.textContent = formatTimestamp(timestamp); messageContentDiv.appendChild(timestampDiv); messageContainer.appendChild(messageContentDiv); if (isSender) { const myPhoto = document.createElement('img'); Object.assign(myPhoto.style, { width: '32px', height: '32px', borderRadius: '50%', objectFit: 'cover', marginTop: '4px' }); const currentUser = firebase.auth().currentUser; if (currentUser) { firebase.database().ref(`profile_photos/${sanitizeEmail(currentUser.email)}`).once('value').then(snapshot => { myPhoto.src = snapshot.exists() ? snapshot.val() : 'profile.png'; }); } messageContainer.appendChild(myPhoto); } insertGroupMessageInOrder(messagesDiv, messageContainer); } let typingTimeout; const TYPING_TIMEOUT_DURATION = 1000; function updateTypingStatus(isTyping) { const currentUser = firebase.auth().currentUser; if (!currentUser || (!selectedUser && !selectedGroupId)) return; const currentUserId = sanitizeEmail(currentUser.email); if (selectedUser) { const sanitizedUser = sanitizeEmail(selectedUser); firebase.database() .ref(`typingStatus/${currentUserId}/${sanitizedUser}`) .set(isTyping); } else if (selectedGroupId) { firebase.database() .ref(`typingStatusGroup/${selectedGroupId}/${currentUserId}`) .set(isTyping); } } document.getElementById('message').addEventListener('input', function () { updateTypingStatus(true); if (typingTimeout) clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { updateTypingStatus(false); }, TYPING_TIMEOUT_DURATION); }); document.getElementById('message').addEventListener('blur', function () { updateTypingStatus(false); }); // Call Functions function initiateCall(callType) { if (!selectedUser) { alert('Please select a user to call'); return; } const currentUser = firebase.auth().currentUser; if (!currentUser) { alert('You must be signed in to make calls'); return; } cleanupCallResources(); currentCallType = callType; isCaller = true; const callContainer = document.getElementById('call-container'); callContainer.style.display = 'flex'; formatName(selectedUser).then(name => { document.getElementById('call-status').textContent = `Calling ${name}...`; }); document.getElementById('call-timer').textContent = '00:00'; document.getElementById('remote-video').style.display = 'block'; document.getElementById('local-video').style.display = currentCallType === 'video' ? 'block' : 'none'; currentCallId = firebase.database().ref().child('calls').push().key; console.log('Starting new call with ID:', currentCallId); const constraints = { audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }, video: callType === 'video' ? { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } } : false }; navigator.mediaDevices.getUserMedia(constraints) .then(stream => { localStream = stream; const localVideo = document.getElementById('local-video'); localVideo.srcObject = stream; localVideo.muted = true; createPeerConnection(); sendCallRequest(); }) .catch(error => { console.error('Error accessing media devices:', error); endCall(); let errorMessage = 'Could not access microphone or camera. Please check permissions.'; if (error.name === 'NotFoundError' || error.name === 'DevicesNotFoundError') { errorMessage = 'Required hardware not found. Please check your camera/microphone.'; } else if (error.name === 'NotReadableError' || error.name === 'TrackStartError') { errorMessage = 'Hardware is in use by another application.'; } else if (error.name === 'OverconstrainedError' || error.name === 'ConstraintNotSatisfiedError') { errorMessage = 'Constraints could not be satisfied by available devices.'; } else if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') { errorMessage = 'Permission denied. Please allow camera/microphone access.'; } else if (error.name === 'TypeError' || error.name === 'TypeError') { errorMessage = 'Type error occurred while accessing media devices.'; } alert(errorMessage); }); } function createPeerConnection() { if (peerConnection) { cleanupCallResources(); } const configuration = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, { urls: 'stun:stun2.l.google.com:19302' }, { urls: 'stun:stun3.l.google.com:19302' }, { urls: 'stun:stun4.l.google.com:19302' } ], iceCandidatePoolSize: 10, iceTransportPolicy: 'all' }; peerConnection = new RTCPeerConnection(configuration); console.log('Created new PeerConnection'); if (localStream) { localStream.getTracks().forEach(track => { peerConnection.addTrack(track, localStream); }); } peerConnection.onicecandidate = handleICECandidateEvent; peerConnection.ontrack = handleTrackEvent; peerConnection.oniceconnectionstatechange = handleICEConnectionStateChange; peerConnection.onicegatheringstatechange = handleICEGatheringStateChange; peerConnection.onsignalingstatechange = handleSignalingStateChange; peerConnection.onnegotiationneeded = handleNegotiationNeeded; peerConnection.onconnectionstatechange = handleConnectionStateChange; if (!isCaller) { peerConnection.ondatachannel = event => { callChannel = event.channel; callChannel.onopen = () => { console.log('Data channel opened for receiver'); setupCallButtons(); }; callChannel.onmessage = handleControlMessage; }; } } function handleConnectionStateChange() { if (peerConnection) { console.log('Connection state changed:', peerConnection.connectionState); if (peerConnection.connectionState === 'disconnected' || peerConnection.connectionState === 'failed') { endCall(); } } } function sendCallRequest() { const currentUser = firebase.auth().currentUser; const callData = { caller: currentUser.email, callee: selectedUser, callId: currentCallId, type: currentCallType, timestamp: Date.now(), status: 'ringing' }; callRequestRef = firebase.database().ref(`calls/${currentCallId}`); callRequestRef.set(callData); callStatusRef = firebase.database().ref(`calls/${currentCallId}/status`); callStatusRef.on('value', snapshot => { const status = snapshot.val(); if (status === 'answered') { formatName(selectedUser).then(name => { document.getElementById('call-status').textContent = `Call connected with ${name}`; }); startCallTimer(); createDataChannel(); createOffer(); setupCallButtons(); } else if (status === 'rejected') { endCall(); formatName(selectedUser).then(name => { alert(`${name} rejected your call.`); }); } else if (status === 'ended') { endCall(); } }); iceCandidatesRef = firebase.database().ref(`calls/${currentCallId}/calleeCandidates`); iceCandidatesRef.on('child_added', snapshot => { if (!peerConnection) return; const candidate = new RTCIceCandidate(snapshot.val()); peerConnection.addIceCandidate(candidate) .catch(error => { console.error('Error adding received ICE candidate:', error); }); }); setTimeout(() => { if (document.getElementById('call-container').style.display === 'flex' && document.getElementById('call-status').textContent.includes('Calling')) { endCall(); formatName(selectedUser).then(name => { alert(`${name} is not answering.`); }); } }, 45000); } function createDataChannel() { callChannel = peerConnection.createDataChannel('chat'); callChannel.onopen = () => { console.log('Data channel opened'); setupCallButtons(); }; callChannel.onclose = () => { console.log('Data channel closed'); }; callChannel.onmessage = event => { try { const message = JSON.parse(event.data); if (message.type === 'audio') { const muteIndicator = document.getElementById('remote-mute-indicator'); if (muteIndicator) { muteIndicator.style.display = message.enabled ? 'none' : 'block'; } } else if (message.type === 'video') { const remoteVideo = document.getElementById('remote-video'); if (remoteVideo) { remoteVideo.style.backgroundColor = message.enabled ? 'transparent' : '#333'; } } } catch (e) { console.log('Message from data channel:', event.data); } }; } function createOffer() { peerConnection.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: currentCallType === 'video' }) .then(offer => { return peerConnection.setLocalDescription(offer); }) .then(() => { firebase.database().ref(`calls/${currentCallId}/offer`).set({ sdp: peerConnection.localDescription.sdp, type: peerConnection.localDescription.type }); }) .then(() => { answerRef = firebase.database().ref(`calls/${currentCallId}/answer`); answerRef.on('value', snapshot => { const answer = snapshot.val(); if (answer && peerConnection) { peerConnection.setRemoteDescription(new RTCSessionDescription(answer)) .catch(error => { console.error('Error setting remote description:', error); }); } }); }) .catch(error => { console.error('Error creating offer:', error); endCall(); }); } function handleICECandidateEvent(event) { if (event.candidate && currentCallId) { const candidatePath = isCaller ? 'callerCandidates' : 'calleeCandidates'; console.log('Sending ICE candidate:', event.candidate); firebase.database().ref(`calls/${currentCallId}/${candidatePath}`).push(event.candidate.toJSON()) .catch(error => { console.error('Error sending ICE candidate:', error); }); } else if (!event.candidate) { console.log('All ICE candidates have been sent'); } } function handleTrackEvent(event) { remoteStream = event.streams[0]; const remoteVideo = document.getElementById('remote-video'); remoteVideo.srcObject = remoteStream; formatName(selectedUser).then(name => { document.getElementById('remote-label').textContent = name; }); } function handleICEConnectionStateChange() { if (peerConnection) { console.log('ICE connection state changed:', peerConnection.iceConnectionState); if (peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'failed') { endCall(); } } } function handleICEGatheringStateChange() { if (peerConnection) { console.log('ICE gathering state changed:', peerConnection.iceGatheringState); } } function handleSignalingStateChange() { if (peerConnection) { console.log('Signaling state changed:', peerConnection.signalingState); } } function handleNegotiationNeeded() { console.log('Negotiation needed'); } function startCallTimer() { callStartTime = Date.now(); callTimer = setInterval(updateCallTimer, 1000); } function updateCallTimer() { const elapsed = Math.floor((Date.now() - callStartTime) / 1000); const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0'); const seconds = (elapsed % 60).toString().padStart(2, '0'); document.getElementById('call-timer').textContent = `${minutes}:${seconds}`; } function endCall() { console.log('Ending call...'); if (currentCallId) { if (isCaller) { firebase.database().ref(`calls/${currentCallId}/status`).set('ended'); } else { firebase.database().ref(`calls/${currentCallId}/status`).set('ended'); } setTimeout(() => { firebase.database().ref(`calls/${currentCallId}`).remove().catch(error => { console.error('Error removing call data:', error); }); }, 1000); } cleanupCallResources(); document.getElementById('call-container').style.display = 'none'; if (currentIncomingCallId) { currentIncomingCallId = null; } document.getElementById('incoming-call-modal').style.display = 'none'; } function setupIncomingCallListener() { const currentUser = firebase.auth().currentUser; if (!currentUser) return; incomingCallListener = firebase.database().ref('calls').orderByChild('callee').equalTo(currentUser.email).on('child_added', snapshot => { const callData = snapshot.val(); if (callData.status === 'ringing' && !currentCallId) { handleIncomingCall(callData, snapshot.key); } }); } function handleIncomingCall(callData, callId) { if (currentCallId) { firebase.database().ref(`calls/${callId}/status`).set('rejected'); return; } currentIncomingCallId = callId; const modal = document.getElementById('incoming-call-modal'); const callText = document.getElementById('incoming-call-text'); formatName(callData.caller).then(name => { callText.textContent = `${name} is calling you for a ${callData.type} call`; }); modal.style.display = 'block'; setTimeout(() => { if (modal.style.display === 'block') { rejectIncomingCall(); } }, 60000); } function acceptIncomingCall() { const modal = document.getElementById('incoming-call-modal'); modal.style.display = 'none'; const callId = currentIncomingCallId; currentCallId = callId; firebase.database().ref(`calls/${callId}`).once('value').then(snapshot => { const callData = snapshot.val(); currentCallType = callData.type; isCaller = false; selectedUser = callData.caller; firebase.database().ref(`calls/${callId}/status`).set('answered'); const callContainer = document.getElementById('call-container'); callContainer.style.display = 'flex'; formatName(callData.caller).then(name => { document.getElementById('call-status').textContent = `In call with ${name}`; }); document.getElementById('remote-video').style.display = 'block'; document.getElementById('local-video').style.display = currentCallType === 'video' ? 'block' : 'none'; const constraints = { audio: true, video: currentCallType === 'video' ? { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } } : false }; navigator.mediaDevices.getUserMedia(constraints) .then(stream => { localStream = stream; document.getElementById('local-video').srcObject = stream; createPeerConnection(); offerRef = firebase.database().ref(`calls/${callId}/offer`); offerRef.on('value', offerSnapshot => { const offer = offerSnapshot.val(); if (offer && peerConnection && !peerConnection.currentRemoteDescription) { peerConnection.setRemoteDescription(new RTCSessionDescription(offer)) .then(() => { return peerConnection.createAnswer(); }) .then(answer => { return peerConnection.setLocalDescription(answer); }) .then(() => { firebase.database().ref(`calls/${callId}/answer`).set({ sdp: peerConnection.localDescription.sdp, type: peerConnection.localDescription.type }); startCallTimer(); }) .catch(error => { console.error('Error handling offer:', error); endCall(); }); } }); iceCandidatesRef = firebase.database().ref(`calls/${callId}/callerCandidates`); iceCandidatesRef.on('child_added', candidateSnapshot => { if (!peerConnection) return; const candidate = candidateSnapshot.val(); peerConnection.addIceCandidate(new RTCIceCandidate(candidate)) .catch(error => { console.error('Error adding ICE candidate:', error); }); }); peerConnection.ondatachannel = event => { callChannel = event.channel; setupCallButtons(); callChannel.onopen = () => console.log('Data channel opened'); callChannel.onclose = () => console.log('Data channel closed'); callChannel.onmessage = event => console.log('Received message:', event.data); }; }) .catch(error => { console.error('Error accessing media devices:', error); endCall(); alert('Could not access microphone or camera. Please check permissions.'); }); }); } function rejectIncomingCall() { const modal = document.getElementById('incoming-call-modal'); modal.style.display = 'none'; if (currentIncomingCallId) { firebase.database().ref(`calls/${currentIncomingCallId}/status`).set('rejected'); currentIncomingCallId = null; } } function setupCallButtons() { document.getElementById('end-call-btn').addEventListener('click', endCall); document.getElementById('mute-btn').addEventListener('click', function() { if (localStream) { const audioTracks = localStream.getAudioTracks(); if (audioTracks.length > 0) { const isMuted = !audioTracks[0].enabled; audioTracks[0].enabled = isMuted; this.classList.toggle('active', !isMuted); this.title = isMuted ? 'Mute' : 'Unmute'; this.innerHTML = isMuted ? '' : ''; const localMuteIndicator = document.getElementById('local-mute-indicator'); if (localMuteIndicator) { localMuteIndicator.style.display = isMuted ? 'none' : 'block'; } if (callChannel && callChannel.readyState === 'open') { callChannel.send(JSON.stringify({ type: 'audio', enabled: isMuted })); } } } }); document.getElementById('video-off-btn').addEventListener('click', function() { if (localStream) { const videoTracks = localStream.getVideoTracks(); if (videoTracks.length > 0) { const isVideoOff = !videoTracks[0].enabled; videoTracks[0].enabled = isVideoOff; this.classList.toggle('active', !isVideoOff); this.title = isVideoOff ? 'Turn off video' : 'Turn on video'; this.innerHTML = isVideoOff ? '' : ''; if (callChannel && callChannel.readyState === 'open') { callChannel.send(JSON.stringify({ type: 'video', enabled: isVideoOff })); } } } }); if (callChannel) { callChannel.onmessage = event => { try { const message = JSON.parse(event.data); if (message.type === 'audio') { const muteIndicator = document.getElementById('remote-mute-indicator'); if (muteIndicator) { muteIndicator.style.display = message.enabled ? 'none' : 'block'; } } else if (message.type === 'video') { const remoteVideo = document.getElementById('remote-video'); if (remoteVideo) { remoteVideo.style.backgroundColor = message.enabled ? 'transparent' : '#333'; } } } catch (e) { console.log('Received non-JSON message:', event.data); } }; } } // Group Chat Functions function showCreateGroupModal() { const currentUser = firebase.auth().currentUser; if (!currentUser) return; document.getElementById('group-name').value = ''; document.getElementById('group-avatar-input').value = ''; document.getElementById('group-avatar-preview').innerHTML = ''; document.getElementById('group-avatar-preview').style.backgroundImage = 'none'; const friendsRef = firebase.database().ref(`friends/${currentUser.uid}`); const membersList = document.getElementById('group-members-list'); membersList.innerHTML = ''; friendsRef.once('value').then(snapshot => { snapshot.forEach(childSnapshot => { const friendEmail = childSnapshot.val(); const sanitizedEmail = sanitizeEmail(friendEmail); const checkboxItem = document.createElement('div'); checkboxItem.classList.add('checkbox-item'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `member-${sanitizedEmail}`; checkbox.value = friendEmail; const label = document.createElement('label'); label.htmlFor = `member-${sanitizedEmail}`; label.textContent = friendEmail.split('@')[0].replace(/\./g, ' '); firebase.database().ref(`usernames/${sanitizedEmail}`).once('value').then(nameSnapshot => { if (nameSnapshot.exists()) { label.textContent = nameSnapshot.val(); } }); checkboxItem.appendChild(checkbox); checkboxItem.appendChild(label); membersList.appendChild(checkboxItem); }); }); document.getElementById('create-group-modal').style.display = 'flex'; document.getElementById('group-avatar-input').addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; if (!file.type.match('image.*')) { alert('Please select an image file'); return; } if (file.size > 5 * 1024 * 1024) { alert('File size should be less than 5MB'); return; } const reader = new FileReader(); reader.onload = function(event) { const groupAvatar = document.getElementById('group-avatar-preview'); groupAvatar.style.backgroundImage = `url(${event.target.result})`; groupAvatar.style.backgroundSize = 'cover'; groupAvatar.style.backgroundPosition = 'center'; groupAvatar.innerHTML = ''; currentGroupAvatar = event.target.result; }; reader.readAsDataURL(file); }); } function createGroup() { const currentUser = firebase.auth().currentUser; if (!currentUser) return; const groupName = document.getElementById('group-name').value.trim(); if (!groupName) { alert('Please enter a group name'); return; } const checkboxes = document.querySelectorAll('#group-members-list input[type="checkbox"]:checked'); if (checkboxes.length === 0) { alert('Please select at least one member'); return; } const members = Array.from(checkboxes).map(checkbox => checkbox.value); members.push(currentUser.email); const groupData = { name: groupName, createdBy: currentUser.email, createdAt: Date.now(), lastMessageTimestamp: 0, members: {} }; if (currentGroupAvatar) { groupData.avatar = currentGroupAvatar; } members.forEach(member => { groupData.members[sanitizeEmail(member)] = member; }); const newGroupRef = firebase.database().ref('groups').push(); newGroupRef.set(groupData) .then(() => { const updates = {}; members.forEach(member => { updates[`groupMembers/${sanitizeEmail(member)}/${newGroupRef.key}`] = true; }); return firebase.database().ref().update(updates); }) .then(() => { alert('Group created successfully!'); closeModal('create-group-modal'); loadFriends(); startGroupChat(newGroupRef.key); }) .catch(error => { console.error('Error creating group:', error); alert('Failed to create group'); }); } function showGroupInfoModal(groupId) { const currentUser = firebase.auth().currentUser; if (!currentUser) return; firebase.database().ref(`groups/${groupId}`).once('value').then(snapshot => { const groupData = snapshot.val(); document.getElementById('group-info-name').textContent = groupData.name; const groupAvatar = document.getElementById('group-info-avatar'); if (groupData.avatar) { groupAvatar.style.backgroundImage = `url(${groupData.avatar})`; groupAvatar.style.backgroundSize = 'cover'; groupAvatar.style.backgroundPosition = 'center'; groupAvatar.innerHTML = ''; } else { const initials = groupData.name.split(' ').map(word => word[0]).join('').substring(0, 2); groupAvatar.textContent = initials.toUpperCase(); groupAvatar.style.backgroundImage = 'none'; } const membersCount = Object.keys(groupData.members).length; document.getElementById('group-info-members-count').textContent = `${membersCount} members`; const membersContainer = document.getElementById('group-members-container'); membersContainer.innerHTML = ''; Object.values(groupData.members).forEach(memberEmail => { const sanitizedEmail = sanitizeEmail(memberEmail); const memberDiv = document.createElement('div'); memberDiv.classList.add('group-member'); const memberAvatar = document.createElement('img'); memberAvatar.classList.add('group-member-avatar'); firebase.database().ref(`profile_photos/${sanitizedEmail}`).once('value').then(photoSnapshot => { if (photoSnapshot.exists()) { memberAvatar.src = photoSnapshot.val(); } else { memberAvatar.src = 'profile.png'; } }); const memberName = document.createElement('span'); memberName.textContent = memberEmail.split('@')[0].replace(/\./g, ' '); firebase.database().ref(`usernames/${sanitizedEmail}`).once('value').then(nameSnapshot => { if (nameSnapshot.exists()) { memberName.textContent = nameSnapshot.val(); } }); memberDiv.appendChild(memberAvatar); memberDiv.appendChild(memberName); membersContainer.appendChild(memberDiv); }); document.getElementById('group-info-modal').style.display = 'flex'; }); } function showAddMembersModal() { const currentUser = firebase.auth().currentUser; if (!currentUser || !selectedGroupId) return; const membersList = document.getElementById('add-members-list'); membersList.innerHTML = ''; firebase.database().ref(`friends/${currentUser.uid}`).once('value').then(friendsSnapshot => { firebase.database().ref(`groups/${selectedGroupId}/members`).once('value').then(membersSnapshot => { const currentMembers = membersSnapshot.val() || {}; let hasFriendsToAdd = false; friendsSnapshot.forEach(childSnapshot => { const friendEmail = childSnapshot.val(); const sanitized = sanitizeEmail(friendEmail); if (currentMembers[sanitized]) return; hasFriendsToAdd = true; const checkboxItem = document.createElement('div'); checkboxItem.classList.add('checkbox-item'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `add-${sanitized}`; checkbox.value = friendEmail; const label = document.createElement('label'); label.htmlFor = `add-${sanitized}`; label.textContent = friendEmail.split('@')[0].replace(/\./g, ' '); firebase.database().ref(`usernames/${sanitized}`).once('value').then(nameSnapshot => { if (nameSnapshot.exists()) { label.textContent = nameSnapshot.val(); } }); checkboxItem.appendChild(checkbox); checkboxItem.appendChild(label); membersList.appendChild(checkboxItem); }); if (!hasFriendsToAdd) { membersList.innerHTML = '

No friends available to add to this group.

'; } document.getElementById('add-members-modal').style.display = 'flex'; }); }); } function showEditGroupModal() { if (!selectedGroupId) return; firebase.database().ref(`groups/${selectedGroupId}`).once('value').then(snapshot => { const groupData = snapshot.val(); document.getElementById('edit-group-name').value = groupData.name; const groupAvatar = document.getElementById('edit-group-avatar'); if (groupData.avatar) { groupAvatar.style.backgroundImage = `url(${groupData.avatar})`; groupAvatar.style.backgroundSize = 'cover'; groupAvatar.style.backgroundPosition = 'center'; groupAvatar.innerHTML = ''; currentGroupAvatar = groupData.avatar; } else { const initials = groupData.name.split(' ').map(word => word[0]).join('').substring(0, 2); groupAvatar.textContent = initials.toUpperCase(); groupAvatar.style.backgroundImage = 'none'; currentGroupAvatar = null; } document.getElementById('edit-group-avatar-input').addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; if (!file.type.match('image.*')) { alert('Please select an image file'); return; } if (file.size > 5 * 1024 * 1024) { alert('File size should be less than 5MB'); return; } const reader = new FileReader(); reader.onload = function(event) { const groupAvatar = document.getElementById('edit-group-avatar'); groupAvatar.style.backgroundImage = `url(${event.target.result})`; groupAvatar.style.backgroundSize = 'cover'; groupAvatar.style.backgroundPosition = 'center'; groupAvatar.innerHTML = ''; currentGroupAvatar = event.target.result; }; reader.readAsDataURL(file); }); document.getElementById('edit-group-modal').style.display = 'flex'; }); } function updateGroupInfo() { if (!selectedGroupId) return; const groupName = document.getElementById('edit-group-name').value.trim(); if (!groupName) { alert('Please enter a group name'); return; } const updates = { name: groupName }; if (currentGroupAvatar) { updates.avatar = currentGroupAvatar; } firebase.database().ref(`groups/${selectedGroupId}`).update(updates) .then(() => { alert('Group updated successfully!'); closeModal('edit-group-modal'); if (selectedGroupId) { const chatHeader = document.getElementById('chat-header'); const nameElement = chatHeader.querySelector('h2'); if (nameElement) { nameElement.textContent = groupName; } const groupAvatar = chatHeader.querySelector('div'); if (groupAvatar && currentGroupAvatar) { groupAvatar.style.backgroundImage = `url(${currentGroupAvatar})`; groupAvatar.textContent = ''; } } loadFriends(); }) .catch(error => { console.error('Error updating group:', error); alert('Failed to update group'); }); } function leaveGroup() { const currentUser = firebase.auth().currentUser; if (!currentUser || !selectedGroupId) return; if (!confirm('Are you sure you want to leave this group?')) { return; } const sanitizedUser = sanitizeEmail(currentUser.email); firebase.database().ref(`groups/${selectedGroupId}/members/${sanitizedUser}`).remove() .then(() => { return firebase.database().ref(`groupMembers/${sanitizedUser}/${selectedGroupId}`).remove(); }) .then(() => { alert('You have left the group'); closeModal('group-info-modal'); if (selectedGroupId) { document.getElementById('chat').style.display = 'none'; selectedGroupId = null; } loadFriends(); }) .catch(error => { console.error('Error leaving group:', error); alert('Failed to leave group'); }); } function closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; } // Heartbeat function to keep status fresh function startHeartbeat() { const user = firebase.auth().currentUser; if (!user) return; const userId = sanitizeEmail(user.email); const userStatusRef = firebase.database().ref(`status/${userId}`); const userStatusTimestampRef = firebase.database().ref(`statusLastUpdated/${userId}`); firebase.database().ref(".info/connected").on("value", function(snapshot) { if (snapshot.val() === true) { userStatusRef.onDisconnect().set('offline'); userStatusTimestampRef.onDisconnect().remove(); } }); heartbeatInterval = setInterval(() => { userStatusRef.set('online'); userStatusTimestampRef.set(Date.now()); }, 20000); window.addEventListener('beforeunload', function () { updateTypingStatus(false); userStatusRef.set('offline'); userStatusTimestampRef.remove(); }); } firebase.auth().onAuthStateChanged(user => { if (user) { const sanitizedEmail = user.email.replace(/[.#$\[\]]/g, '_'); firebase.database().ref('userEmails/' + sanitizedEmail).set(user.uid); console.log("User logged in:", user.email); initUserProfile(user); loadFriends(); setupIncomingCallListener(); loadDarkModePreference(); const userStatusRef = firebase.database().ref(`status/${sanitizeEmail(user.email)}`); const userStatusTimestampRef = firebase.database().ref(`statusLastUpdated/${sanitizeEmail(user.email)}`); userStatusRef.set('online'); userStatusTimestampRef.set(Date.now()); userStatusRef.onDisconnect().set('offline'); userStatusTimestampRef.onDisconnect().remove(); startHeartbeat(); } else { if (heartbeatInterval) { clearInterval(heartbeatInterval); } console.log("No user logged in. Redirecting to login..."); window.location.href = "https://www.computeromaster.rf.gd/Login"; } }); firebase.auth().onAuthStateChanged(user => { if (!user) { if (window.currentTypingStatusRef) { window.currentTypingStatusRef.off(); } } }); window.addEventListener('beforeunload', function() { if (window.currentTypingStatusRef) { window.currentTypingStatusRef.off(); } updateTypingStatus(false); }); function toggleDarkMode() { const body = document.body; const isDarkMode = body.classList.toggle('dark-mode'); const darkModeText = document.getElementById('dark-mode-text'); darkModeText.textContent = isDarkMode ? 'Light Mode' : 'Dark Mode'; saveDarkModePreference(isDarkMode); } function saveDarkModePreference(isDarkMode) { const user = firebase.auth().currentUser; if (!user) return; const userId = sanitizeEmail(user.email); firebase.database().ref(`user_preferences/${userId}/darkMode`).set(isDarkMode) .catch(error => { console.error('Error saving dark mode preference:', error); }); } function loadDarkModePreference() { const user = firebase.auth().currentUser; if (!user) return; const userId = sanitizeEmail(user.email); firebase.database().ref(`user_preferences/${userId}/darkMode`).once('value') .then(snapshot => { const isDarkMode = snapshot.val(); if (isDarkMode) { document.body.classList.add('dark-mode'); if(document.getElementById('dark-mode-text')) { document.getElementById('dark-mode-text').textContent = 'Light Mode'; } } }) .catch(error => { console.error('Error loading dark mode preference:', error); }); } function setupSearch() { const searchInput = document.getElementById('chat-search'); searchInput.addEventListener('input', function() { const searchTerm = this.value.toLowerCase(); const chatButtons = document.querySelectorAll('.friend-button'); chatButtons.forEach(button => { const name = button.querySelector('.friend-name').textContent.toLowerCase(); const lastMessage = button.querySelector('.last-message').textContent.toLowerCase(); if (name.includes(searchTerm) || lastMessage.includes(searchTerm)) { button.style.display = 'flex'; } else { button.style.display = 'none'; } }); }); } document.addEventListener('DOMContentLoaded', function() { setupCallButtons(); setupSearch(); // --- Picker Logic --- const gifPickerBtn = document.getElementById('gif-picker-btn'); const gifPicker = document.getElementById('gif-picker'); const gifSearchInput = document.getElementById('gif-search'); const gifResults = document.getElementById('gif-results'); const emojiPickerBtn = document.getElementById('emoji-picker-btn'); const emojiPickerContainer = document.getElementById('emoji-picker-container'); const emojiPicker = document.querySelector('emoji-picker'); const messageTextarea = document.getElementById('message'); // --- Event Listeners --- gifPickerBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleEmojiPicker(false); // Close emoji picker if open toggleGifPicker(); }); emojiPickerBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleGifPicker(false); // Close gif picker if open toggleEmojiPicker(); }); emojiPicker.addEventListener('emoji-click', event => { messageTextarea.value += event.detail.unicode; messageTextarea.focus(); autoResizeTextarea(); // Resize after adding emoji }); gifSearchInput.addEventListener('input', (e) => { const query = gifSearchInput.value.trim(); if (query.length > 1) { searchGifs(query); } }); document.addEventListener('click', (e) => { if (gifPickerVisible && !gifPicker.contains(e.target) && e.target !== gifPickerBtn) { toggleGifPicker(false); } if (emojiPickerVisible && !emojiPickerContainer.contains(e.target) && e.target !== emojiPickerBtn) { toggleEmojiPicker(false); } }); // --- Textarea Auto-Resize and Enter Key Logic --- const autoResizeTextarea = () => { messageTextarea.style.height = 'auto'; // Temporarily shrink to get correct scrollHeight const computedStyle = getComputedStyle(messageTextarea); const maxHeight = parseInt(computedStyle.maxHeight, 10); const scrollHeight = messageTextarea.scrollHeight; if (scrollHeight > maxHeight) { messageTextarea.style.height = `${maxHeight}px`; messageTextarea.style.overflowY = 'auto'; // Show scrollbar } else { messageTextarea.style.height = `${scrollHeight}px`; messageTextarea.style.overflowY = 'hidden'; // Hide scrollbar } }; messageTextarea.addEventListener('input', autoResizeTextarea); messageTextarea.addEventListener('keydown', function(e) { // If Enter is pressed WITHOUT Shift key, send message if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); // Prevent new line in textarea sendMessage(); } // If Shift+Enter is pressed, the default action (new line) is allowed. }); // --- Toggle Functions --- function toggleGifPicker(forceShow) { gifPickerVisible = forceShow !== undefined ? forceShow : !gifPickerVisible; if (gifPickerVisible) { gifPicker.style.display = 'flex'; if (gifResults.innerHTML.trim() === '') { fetchTrendingGifs(); } } else { gifPicker.style.display = 'none'; } } function toggleEmojiPicker(forceShow) { emojiPickerVisible = forceShow !== undefined ? forceShow : !emojiPickerVisible; emojiPickerContainer.style.display = emojiPickerVisible ? 'block' : 'none'; if (emojiPickerVisible) { // Focus the search bar inside the emoji picker's shadow DOM setTimeout(() => emojiPicker.shadowRoot.querySelector('input[type="search"]').focus(), 100); } } // --- GIF Fetching Functions --- async function fetchTrendingGifs() { gifResults.innerHTML = '

Loading GIFs...

'; try { const response = await fetch(`https://api.giphy.com/v1/gifs/search?api_key=${GIPHY_API_KEY}&q=computer&limit=30&rating=g`); const data = await response.json(); displayGifs(data.data); } catch (error) { console.error('Error fetching trending GIFs:', error); gifResults.innerHTML = '

Could not load GIFs.

'; } } async function searchGifs(query) { gifResults.innerHTML = `

Searching for "${query}"...

`; try { const response = await fetch(`https://api.giphy.com/v1/gifs/search?api_key=${GIPHY_API_KEY}&q=${encodeURIComponent(query)}&limit=30&rating=g`); const data = await response.json(); displayGifs(data.data); } catch (error) { console.error('Error searching GIFs:', error); gifResults.innerHTML = '

Could not load GIFs.

'; } } function displayGifs(gifs) { gifResults.innerHTML = ''; if (gifs.length === 0) { gifResults.innerHTML = '

No results found.

'; return; } gifs.forEach(gif => { const img = document.createElement('img'); img.src = gif.images.fixed_height_small.url; img.alt = gif.title; img.addEventListener('click', () => { sendGif(gif.images.fixed_height.url); toggleGifPicker(false); }); gifResults.appendChild(img); }); } // --- NEW: Drag and Drop Functionality --- const chatArea = document.getElementById('chat'); // Create the overlay div dynamically const overlay = document.createElement('div'); overlay.id = 'drag-drop-overlay'; Object.assign(overlay.style, { position: 'absolute', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 123, 255, 0.7)', color: 'white', display: 'none', // Initially hidden justifyContent: 'center', alignItems: 'center', fontSize: '24px', fontWeight: 'bold', zIndex: '1000', pointerEvents: 'none', // Allows events to pass through to elements underneath border: '2px dashed white', borderRadius: '8px' }); overlay.textContent = 'Drop File to Upload'; // Ensure the chat area can contain the absolutely positioned overlay chatArea.style.position = 'relative'; chatArea.appendChild(overlay); chatArea.addEventListener('dragenter', (e) => { e.preventDefault(); e.stopPropagation(); // Only show the overlay if a chat is selected if (selectedUser || selectedGroupId) { overlay.style.display = 'flex'; } }); chatArea.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); }); chatArea.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); // This check prevents the overlay from flickering when dragging over child elements if (e.relatedTarget === null || !chatArea.contains(e.relatedTarget)) { overlay.style.display = 'none'; } }); chatArea.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); overlay.style.display = 'none'; const files = e.dataTransfer.files; if (files.length > 0) { // Use the centralized handler to process the dropped file handleFileUpload(files[0]); } }); }); document.querySelectorAll('.settings-tab').forEach(tab => { tab.addEventListener('click', function() { document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active')); this.classList.add('active'); document.querySelectorAll('.settings-content').forEach(content => { content.style.display = 'none'; }); const tabName = this.getAttribute('data-tab'); document.getElementById(`${tabName}-tab-content`).style.display = 'block'; }); }); function loadCurrentUsername() { const user = firebase.auth().currentUser; if (!user) return; const userId = sanitizeEmail(user.email); firebase.database().ref(`usernames/${userId}`).once('value').then(snapshot => { const username = snapshot.val(); document.getElementById('username-input').value = username || ''; }); } function updateUsername() { const user = firebase.auth().currentUser; if (!user) return; const newUsername = document.getElementById('username-input').value.trim(); if (!newUsername) return; const userId = sanitizeEmail(user.email); firebase.database().ref(`usernames/${userId}`).set(newUsername) .then(() => { document.getElementById('user-profile-name').textContent = newUsername; updateNameInAllFriendLists(user.email, newUsername); updateChatHeaderName(user.email, newUsername); updateMessageSenderNames(user.email, newUsername); alert('Username updated successfully!'); }) .catch(error => { console.error('Error updating username:', error); alert('Failed to update username'); }); } function updateAllUsernameReferences(newUsername) { document.getElementById('user-profile-name').textContent = newUsername; document.querySelectorAll('.friend-name').forEach(element => { if (element.textContent === formatName(firebase.auth().currentUser.email)) { element.textContent = newUsername; } }); const chatHeader = document.getElementById('chat-header'); if (chatHeader && chatHeader.querySelector('h2').textContent === formatName(firebase.auth().currentUser.email)) { chatHeader.querySelector('h2').textContent = newUsername; } document.querySelectorAll('.sender-name').forEach(element => { if (element.textContent === formatName(firebase.auth().currentUser.email)) { element.textContent = newUsername; } }); } function loadFriendsListForSettings() { const user = firebase.auth().currentUser; if (!user) return; const friendsList = document.querySelector('.friends-list'); if (!friendsList) return; friendsList.innerHTML = ''; firebase.database().ref(`friends/${user.uid}`).once('value').then(snapshot => { snapshot.forEach(childSnapshot => { const friendEmail = childSnapshot.val(); const sanitizedEmail = sanitizeEmail(friendEmail); const friendItem = document.createElement('div'); friendItem.classList.add('friend-item'); const friendInfo = document.createElement('div'); friendInfo.classList.add('friend-info'); const friendAvatar = document.createElement('img'); friendAvatar.classList.add('friend-avatar'); friendAvatar.src = 'profile.png'; firebase.database().ref(`profile_photos/${sanitizedEmail}`).once('value').then(photoSnapshot => { if (photoSnapshot.exists()) { friendAvatar.src = photoSnapshot.val(); } }); const friendName = document.createElement('div'); friendName.classList.add('friend-name'); formatName(friendEmail).then(name => { friendName.textContent = name; }); const unfriendBtn = document.createElement('button'); unfriendBtn.classList.add('unfriend-btn'); unfriendBtn.textContent = 'Unfriend'; unfriendBtn.onclick = () => removeFriend(friendEmail, childSnapshot.key); friendInfo.appendChild(friendAvatar); friendInfo.appendChild(friendName); friendItem.appendChild(friendInfo); friendItem.appendChild(unfriendBtn); friendsList.appendChild(friendItem); }); }); } function removeFriend(friendEmail) { const currentUser = firebase.auth().currentUser; if (!currentUser) { alert("You must be signed in."); return; } const confirmUnfriend = confirm(`Are you sure you want to remove ${friendEmail} from your friends list?`); if (!confirmUnfriend) return; const currentUserId = currentUser.uid; const currentUserEmail = currentUser.email; const sanitizedCurrentEmail = currentUserEmail.replace(/[.#$[\]]/g, '_'); const sanitizedFriendEmail = friendEmail.replace(/[.#$[\]]/g, '_'); const db = firebase.database(); db.ref(`friends/${currentUserId}`).once('value').then(snapshot => { snapshot.forEach(child => { if (child.val() === friendEmail) { db.ref(`friends/${currentUserId}/${child.key}`).remove(); } }); }); db.ref(`userEmails/${sanitizedFriendEmail}`).once('value').then(friendSnap => { const friendUid = friendSnap.val(); if (!friendUid) return; const removeFromFriendList = db.ref(`friends/${friendUid}`).once('value').then(snapshot => { snapshot.forEach(child => { if (child.val() === currentUserEmail) { db.ref(`friends/${friendUid}/${child.key}`).remove(); } }); }); const path1 = `${sanitizedCurrentEmail}/${sanitizedFriendEmail}`; const path2 = `${sanitizedFriendEmail}/${sanitizedCurrentEmail}`; const deleteMessages1 = db.ref(`messages/${path1}`).remove(); const deleteMessages2 = db.ref(`messages/${path2}`).remove(); const deleteUnread1 = db.ref(`unreadMessages/${path1}`).remove(); const deleteUnread2 = db.ref(`unreadMessages/${path2}`).remove(); Promise.all([ removeFromFriendList, deleteMessages1, deleteMessages2, deleteUnread1, deleteUnread2 ]).then(() => { const friendElement = document.querySelector(`.friend-button[data-email="${friendEmail}"]`); if (friendElement) friendElement.remove(); if (typeof loadFriends === 'function') { loadFriends(); } alert(`${friendEmail} removed successfully.`); }); }); } function getUserIdByEmail(email) { return new Promise((resolve, reject) => { firebase.database().ref('users').orderByChild('email').equalTo(email).once('value') .then(snapshot => { if (snapshot.exists()) { resolve(Object.keys(snapshot.val())[0]); } else { resolve(null); } }) .catch(error => { reject(error); }); }); } function formatName(email) { if (!email) return Promise.resolve(''); const sanitizedEmail = sanitizeEmail(email); return new Promise((resolve) => { firebase.database().ref(`usernames/${sanitizedEmail}`).once('value').then(snapshot => { resolve(snapshot.exists() ? snapshot.val() : email.split('@')[0].replace(/\./g, ' ')); }).catch(() => { resolve(email.split('@')[0].replace(/\./g, ' ')); }); }); } document.getElementById('avatar-upload').addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; if (!file.type.match('image.*')) { alert('Please select an image file'); return; } if (file.size > 5 * 1024 * 1024) { alert('File size should be less than 5MB'); return; } const user = firebase.auth().currentUser; if (!user) return; compressImage(file, { quality: 0.7, maxWidth: 400, maxHeight: 400, success: (compressedFile) => { uploadProfilePhotoToDatabase(compressedFile, user); }, error: (err) => { console.error('Error compressing image:', err); alert('Error processing image'); } }); }); document.getElementById('dark-mode-toggle').addEventListener('change', function() { document.body.classList.toggle('dark-mode', this.checked); saveDarkModePreference(this.checked); }); function getUserIdByEmail(email) { return new Promise((resolve, reject) => { firebase.database().ref('users').orderByChild('email').equalTo(email).once('value') .then(snapshot => { if (snapshot.exists()) { resolve(Object.keys(snapshot.val())[0]); } else { resolve(null); } }) .catch(error => { reject(error); }); }); } function toggleSettings() { const container = document.getElementById('user-profile-container'); const settingsIcon = document.getElementById('settings-icon'); container.classList.toggle('extended'); if (container.classList.contains('extended')) { settingsIcon.style.display = 'none'; loadCurrentUsername(); loadFriendsListForSettings(); } else { settingsIcon.style.display = 'flex'; } } document.addEventListener('click', function(e) { const container = document.getElementById('user-profile-container'); const settingsIcon = document.getElementById('settings-icon'); if (!container.contains(e.target) && e.target !== settingsIcon && !settingsIcon.contains(e.target)) { if(container.classList.contains('extended')) { container.classList.remove('extended'); settingsIcon.style.display = 'flex'; } } }); function updateNameInAllFriendLists(userEmail, newName) { const friendButtons = document.querySelectorAll('.friend-button'); friendButtons.forEach(button => { if (button.getAttribute('data-email') === userEmail) { const nameElement = button.querySelector('.friend-name'); if (nameElement) { nameElement.textContent = newName; } } }); } function updateChatHeaderName(userEmail, newName) { const chatHeader = document.getElementById('chat-header'); if (chatHeader && selectedUser === userEmail) { const nameElement = chatHeader.querySelector('h2'); if (nameElement) { nameElement.textContent = newName; } } } function updateMessageSenderNames(userEmail, newName) { const messages = document.querySelectorAll('.message-sender-name'); messages.forEach(senderName => { formatName(userEmail).then(formattedName => { if (senderName.textContent === formattedName) { senderName.textContent = newName; } }); }); } function updateSidebarMessage(userEmail, messageText, timestamp) { const button = document.querySelector(`button[data-email="${userEmail}"]`); if (button) { const lastMessageSpan = button.querySelector('.last-message'); if (lastMessageSpan) { // Remove
tags and replace with space for sidebar display lastMessageSpan.textContent = messageText.replace(/
/g, ' '); } const usersContainer = document.getElementById('users'); usersContainer.insertBefore(button, usersContainer.firstChild); } } // FIXED: Removed duplicate function definition function updateSidebarGroupMessage(groupId, messageText, timestamp) { const button = document.querySelector(`button[data-group-id="${groupId}"]`); if (button) { const lastMessageSpan = button.querySelector('.last-message'); if (lastMessageSpan) { lastMessageSpan.textContent = messageText; } const usersContainer = document.getElementById('users'); usersContainer.insertBefore(button, usersContainer.firstChild); } } function insertGroupMessageInOrder(container, newMessage) { const newTime = parseInt(newMessage.dataset.timestamp || '0'); let inserted = false; for (const child of container.children) { const childTime = parseInt(child.dataset.timestamp || '0'); if (newTime < childTime) { container.insertBefore(newMessage, child); inserted = true; break; } } if (!inserted) { container.appendChild(newMessage); } }