// ==================== // Timezone utilities // ==================== function getUserTimezone() { return Intl.DateTimeFormat().resolvedOptions().timeZone; } function parseUTCToLocal(utcTimestamp) { if (!utcTimestamp) return new Date(); let isoString = utcTimestamp.replace(' ', 'T'); if (!isoString.endsWith('Z') && !isoString.includes('+')) { isoString += 'Z'; } return new Date(isoString); } function formatLocalTime(utcTimestamp) { const localDate = parseUTCToLocal(utcTimestamp); const dateStr = localDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); const timeStr = localDate.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); return { dateStr, timeStr }; } // ==================== // Messaging and chat related logic // ==================== let socket = null; let isSending = false; function connectSocketIO() { if (!token) return; if (socket) { socket.disconnect(); } console.log('Connecting to SocketIO...'); try { // Connect to the same host/port as the HTTP requests const socketUrl = window.location.origin; socket = io(socketUrl, { transports: ['websocket', 'polling'] }); socket.on('connect', () => { console.log('SocketIO connected'); }); socket.on('new_message', (message) => { console.log('SocketIO: New message received:', message); handleSocketMessage(message); }); socket.on('user_ban_status', (data) => { console.log('SocketIO: User ban status:', data); if (data.user_id == userId && data.is_banned) { // Current user was banned, disconnect alert('You have been banned from the chat.'); logout(); } }); socket.on('delete_message', (data) => { console.log('SocketIO: Delete message:', data); // Remove the message from the UI const messageElement = document.querySelector(`[data-msg-id="${data.id}"]`); if (messageElement) { messageElement.remove(); } }); socket.on('system_notification', (data) => { console.log('SocketIO: System notification:', data); // Show system notification showBrowserNotification('System Notification', data.message, null, null); // Could also display in chat if needed }); socket.on('disconnect', () => { console.log('SocketIO disconnected'); }); socket.on('connect_error', (error) => { console.error('SocketIO connection error:', error); // Try to reconnect after a delay setTimeout(() => { if (!socket.connected) { console.log('Retrying SocketIO connection...'); connectSocketIO(); } }, 5000); }); } catch (e) { console.error('SocketIO: Error creating connection:', e); } } function handleSocketMessage(message) { if (message.sender_id == userId) return; let key = 'global'; if (message.channel_id) { key = conversationKey('c', message.channel_id); } else if (message.is_global != 1) { const otherUserId = message.sender_id == userId ? message.receiver_id : message.sender_id; key = conversationKey('d', otherUserId); } if (shouldNotifyMessage(message, key)) { let title = ''; let body = ''; if (message.channel_id) { let channelName = 'Channel'; let serverName = 'Server'; servers.forEach(s => { const ch = s.channels.find(c => c.id == message.channel_id); if (ch) { channelName = ch.name; serverName = s.name; } }); title = `${serverName} - #${channelName}`; } else if (message.is_global == 1) { title = 'Global Chat'; } else { title = `DM from ${message.username}`; } let contentPreview = ''; try { const parsed = JSON.parse(message.content); contentPreview = parsed.text || parsed.file?.filename || '[File]'; } catch (e) { contentPreview = message.content; } body = contentPreview.substring(0, 100); if (contentPreview.length > 100) body += '...'; showBrowserNotification(title, body, message.avatar_url || null, null); markMessageNotified(message.id); } const ts = new Date(message.timestamp).getTime(); if (!lastSeen[key] || ts > lastSeen[key]) { unreadCounts[key] = (unreadCounts[key] || 0) + 1; lastSeen[key] = ts; renderUnreadIndicators(); } // Add the message to the UI if it's for the current view //console.log('Attempting to display message:', message, 'currentChannelId:', currentChannelId, 'currentDMUserId:', currentDMUserId); displayMessages([message]); } function disconnectSocketIO() { if (socket) { socket.disconnect(); socket = null; } } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } async function requestNotificationPermission() { if (!('Notification' in window)) { console.log('This browser does not support notifications'); return false; } if (Notification.permission === 'granted') { return true; } if (Notification.permission !== 'denied') { const permission = await Notification.requestPermission(); notificationPermission = permission; return permission === 'granted'; } return false; } function showBrowserNotification(title, body, icon = null, onClick = null) { if (notificationPermission !== 'granted') { return; } const options = { body: body, icon: icon || '/static/avatars/avatar_1.png', badge: '/static/avatars/avatar_1.png', tag: title, requireInteraction: false }; const notification = new Notification(title, options); if (onClick) { notification.onclick = function() { window.focus(); onClick(); notification.close(); }; } setTimeout(() => { notification.close(); }, 5000); } function shouldNotifyMessage(msg, conversationKey) { if (msg.sender_id == userId) return false; const isCurrentConversation = ( (currentDMUserId && conversationKey === `d${currentDMUserId}`) || (currentChannelId && conversationKey === `c${currentChannelId}`) || (!currentDMUserId && !currentChannelId && conversationKey === 'global') ); if (isCurrentConversation && document.visibilityState === 'visible') { return false; } const msgKey = `msg_${msg.id}`; if (lastNotifiedMessages[msgKey]) { return false; } return true; } function markMessageNotified(msgId) { lastNotifiedMessages[`msg_${msgId}`] = Date.now(); const keys = Object.keys(lastNotifiedMessages); if (keys.length > 100) { const cutoff = Date.now() - (24 * 60 * 60 * 1000); keys.forEach(key => { if (lastNotifiedMessages[key] < cutoff) { delete lastNotifiedMessages[key]; } }); } } function renderMessageContent(content) { let parsed; try { parsed = JSON.parse(content); } catch (e) { parsed = null; } if (parsed && parsed.file) { const file = parsed.file; let html = ''; if (parsed.text) { html += escapeHtml(parsed.text) + '
'; } const url = file.url; const mime = file.mimetype || ''; if (mime.startsWith('image/')) { html += ``; } else if (mime.startsWith('video/')) { html += ``; } else if (mime.startsWith('audio/')) { html += ``; } else { html += `${escapeHtml(file.filename)} (${formatBytes(file.size)})`; } return html; } return escapeHtml(content); } async function sendMessage() { if (isSending) return; const text = messageInput.value.trim(); const file = fileInput ? fileInput.files[0] : null; if (!text && !file) return; isSending = true; sendBtn.disabled = true; sendBtn.classList.add('btn-disabled'); messageInput.readOnly = true; messageInput.classList.add('input-disabled'); try { let res; if (file) { const form = new FormData(); if (text) form.append('content', text); form.append('file', file); if (currentDMUserId) form.append('receiver_id', currentDMUserId); if (currentChannelId) form.append('channel_id', currentChannelId); if (replyToMessage) form.append('reply_to', replyToMessage.id); res = await fetch(`${API_URL}/messages`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: form }); } else { const body = { content: text }; if (currentDMUserId) body.receiver_id = currentDMUserId; if (currentChannelId) body.channel_id = currentChannelId; if (replyToMessage) body.reply_to = replyToMessage.id; res = await fetch(`${API_URL}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }); } if (res && res.ok) { messageInput.value = ''; fileInput.value = ''; filePreview.style.display = 'none'; cancelReply(); if (currentDMUserId) { loadDM(currentDMUserId); } else { loadChannel(currentChannelId); } } else if (res) { const err = await res.json(); console.error('sendMessage error response', err); } } catch (error) { console.error('Error sending message:', error); } finally { isSending = false; sendBtn.disabled = false; sendBtn.classList.remove('btn-disabled'); messageInput.readOnly = false; messageInput.classList.remove('input-disabled'); } } async function loadChannel(channelId) { try { let url = `${API_URL}/messages?channel_id=${channelId}`; if (lastFetchedTs) url += `&since=${encodeURIComponent(lastFetchedTs)}`; const res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const messages = await res.json(); displayMessages(messages, !!lastFetchedTs); } } catch (error) { console.error('Error loading channel messages:', error); } } async function loadGlobalChat() { if (currentChannelId) return loadChannel(currentChannelId); try { const res = await fetch(`${API_URL}/messages`, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const messages = await res.json(); displayMessages(messages); } } catch (error) { console.error('Error loading messages:', error); } } async function loadUsers() { try { const res = await fetch(`${API_URL}/users`, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { allUsers = await res.json(); } } catch (error) { console.error('Error loading users:', error); } } async function loadDM(userId) { try { const res = await fetch(`${API_URL}/dm/${userId}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const messages = await res.json(); displayMessages(messages); } } catch (error) { console.error('Error loading DM:', error); } } let lastFetchedTs = null; let isAtBottom = true; function displayMessages(messages, append = false) { const existing = new Set(); [...messagesContainer.querySelectorAll('.message')].forEach(el => { const id = el.getAttribute('data-msg-id'); if (id) existing.add(String(id)); }); messages.forEach(msg => { //console.log('Processing message:', msg.id, 'channel_id:', msg.channel_id, 'currentChannelId:', currentChannelId, 'currentDMUserId:', currentDMUserId, 'is_global:', msg.is_global); if (typeof currentChannelId !== 'undefined' && currentChannelId) { if (String(msg.channel_id || '') !== String(currentChannelId)) { console.log('Message filtered out: channel mismatch'); return; } } else if (typeof currentDMUserId !== 'undefined' && currentDMUserId) { const rid = msg.receiver_id || msg.receiver || null; if (!(String(msg.sender_id) === String(currentDMUserId) || String(rid) === String(currentDMUserId))) return; } else { if (msg.is_global === 0 || msg.is_global === '0') return; } const msgId = String(msg.id || msg._id || msg.timestamp); if (existing.has(msgId)) { return; } const msgEl = document.createElement('div'); msgEl.className = `message ${msg.sender_id == userId ? 'own' : ''}`; msgEl.setAttribute('data-msg-id', msgId); msgEl.setAttribute('data-sender-id', msg.sender_id); let avatarHtml = ''; if (msg.avatar_url) { avatarHtml = `avatar`; } let badgeHtml = ''; if (msg.badge) { const badgeClass = msg.badge.toLowerCase().replace(/\s+/g, '-'); badgeHtml = `${msg.badge}`; } let pron = msg.pronouns ? ` (${msg.pronouns})` : ''; const { dateStr, timeStr } = formatLocalTime(msg.timestamp); const renderedContent = renderMessageContent(msg.content); let replyIndicatorHtml = ''; if (msg.reply_to) { replyIndicatorHtml = `
Replying to message #${msg.reply_to}
`; } // FIXED: Proper Telegram-style message structure msgEl.innerHTML = `
${avatarHtml}
${msg.username}${badgeHtml}${pron} ${dateStr} ${timeStr}
${replyIndicatorHtml}
${renderedContent}
`; messagesContainer.appendChild(msgEl); //console.log('Message added to DOM:', msgId); }); if (isAtBottom) { requestAnimationFrame(() => { messagesContainer.scrollTop = messagesContainer.scrollHeight; }); } if (messages.length) { const lastTs = new Date(messages[messages.length - 1].timestamp).toISOString(); lastFetchedTs = lastTs; let key; if (currentDMUserId) { key = conversationKey('d', currentDMUserId); } else if (currentChannelId) { key = conversationKey('c', currentChannelId); } else { key = 'global'; } lastSeen[key] = new Date(lastTs).getTime(); unreadCounts[key] = 0; renderUnreadIndicators(); } } let autoRefreshInterval = null; function startAutoRefresh() { if (autoRefreshInterval) { clearInterval(autoRefreshInterval); } autoRefreshInterval = setInterval(async () => { const now = Date.now(); if (!window.lastFriendRefresh || now - window.lastFriendRefresh > 10000) { await loadFriendRequests(); await loadFriends(); window.lastFriendRefresh = now; } }, 5000); } function stopAutoRefresh() { if (autoRefreshInterval) { clearInterval(autoRefreshInterval); autoRefreshInterval = null; } window.lastFriendRefresh = null; } function renderUnreadIndicators() { servers.forEach(s => { s.channels.forEach(ch => { const key = conversationKey('c', ch.id); const count = unreadCounts[key]; const el = [...document.querySelectorAll('.channel')].find(n => n.textContent.includes(ch.name)); if (el) { let badge = el.querySelector('.unread-badge'); if (count) { if (!badge) { badge = document.createElement('span'); badge.className = 'unread-badge'; el.appendChild(badge); } badge.textContent = count; } else if (badge) { badge.remove(); } } }); }); displayDMList(); } function initChatListeners() { sendBtn.addEventListener('click', () => { if (isSending) return; sendMessage(); }); messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { if (isSending) return; e.preventDefault(); sendMessage(); } }); attachBtn.addEventListener('click', () => { fileInput.click(); }); fileInput.addEventListener('change', () => { if (fileInput.files && fileInput.files[0]) { const file = fileInput.files[0]; const name = file.name; const size = formatBytes(file.size); filePreviewText.textContent = `📎 ${name} (${size})`; filePreview.style.display = 'flex'; } }); filePreviewClear.addEventListener('click', () => { fileInput.value = ''; filePreview.style.display = 'none'; }); messagesContainer.addEventListener('scroll', () => { const { scrollTop, scrollHeight, clientHeight } = messagesContainer; isAtBottom = scrollHeight - scrollTop - clientHeight < 50; }); messagesContainer.addEventListener('click', (e) => { const replyBtn = e.target.closest('.msg-reply-btn'); if (replyBtn) { const msgId = replyBtn.dataset.msgId; const username = replyBtn.dataset.username; const messageEl = replyBtn.closest('.message'); const contentEl = messageEl.querySelector('.message-content'); let content = contentEl ? contentEl.textContent : ''; if (content.length > 50) { content = content.substring(0, 50) + '...'; } startReply(msgId, username, content); return; } const replyIndicator = e.target.closest('.reply-indicator'); if (replyIndicator) { const replyToId = replyIndicator.dataset.replyTo; if (replyToId) { scrollToMessage(replyToId); } } }); replyCancelBtn.addEventListener('click', cancelReply); } function startReply(msgId, username, content) { replyToMessage = { id: msgId, username: username, content: content }; replyToUsername = username; replyUsername.textContent = username; replyPreviewText.textContent = content; replyPreview.style.display = 'flex'; messageInput.focus(); } function cancelReply() { replyToMessage = null; replyToUsername = null; replyPreview.style.display = 'none'; replyUsername.textContent = ''; replyPreviewText.textContent = ''; } function scrollToMessage(msgId) { const targetMsg = messagesContainer.querySelector(`[data-msg-id="${msgId}"]`); if (targetMsg) { targetMsg.scrollIntoView({ behavior: 'smooth', block: 'center' }); targetMsg.style.transition = 'background-color 0.3s ease'; const originalBg = targetMsg.style.backgroundColor; targetMsg.style.backgroundColor = 'rgba(82, 136, 193, 0.3)'; setTimeout(() => { targetMsg.style.backgroundColor = originalBg; }, 1500); } else { console.log('Message not found:', msgId); } }