document.addEventListener('DOMContentLoaded', () => { const fileUpload = document.getElementById('file-upload'); const chatForm = document.getElementById('chat-form'); const chatInput = document.getElementById('chat-input'); const messagesList = document.getElementById('messages-list'); const typingIndicator = document.getElementById('typing-indicator'); // Emoji list for reactions const EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🔥', '✅', '🚀']; // Scroll to bottom messagesList.scrollTop = messagesList.scrollHeight; const currentChannel = new URLSearchParams(window.location.search).get('channel_id') || 1; let typingTimeout; // WebSocket for real-time let ws; let voiceHandler; function connectWS() { try { ws = new WebSocket('ws://' + window.location.hostname + ':8080'); if (typeof VoiceChannel !== 'undefined') { voiceHandler = new VoiceChannel(ws); } ws.onmessage = (e) => { const msg = JSON.parse(e.data); // Voice signaling if (msg.type && msg.type.startsWith('voice_')) { if (voiceHandler) voiceHandler.handleSignaling(msg); return; } if (msg.type === 'message') { const data = JSON.parse(msg.data); if (data.channel_id == currentChannel) { appendMessage(data); messagesList.scrollTop = messagesList.scrollHeight; } } else if (msg.type === 'typing') { if (msg.channel_id == currentChannel && msg.user_id != window.currentUserId) { showTyping(msg.username); } } else if (msg.type === 'reaction') { updateReactionUI(msg.message_id, msg.reactions); } else if (msg.type === 'message_edit') { const el = document.querySelector(`.message-item[data-id="${msg.message_id}"] .message-text`); if (el) el.innerHTML = msg.content.replace(/\n/g, '
'); } else if (msg.type === 'message_delete') { document.querySelector(`.message-item[data-id="${msg.message_id}"]`)?.remove(); } else if (msg.type === 'presence') { updatePresenceUI(msg.user_id, msg.status); } }; ws.onopen = () => { ws.send(JSON.stringify({ type: 'presence', user_id: window.currentUserId, status: 'online' })); }; ws.onclose = () => setTimeout(connectWS, 3000); } catch (e) { console.warn('WebSocket connection failed.'); } } connectWS(); function showTyping(username) { typingIndicator.textContent = `${username} is typing...`; clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { typingIndicator.textContent = ''; }, 3000); } chatInput.addEventListener('input', () => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'typing', channel_id: currentChannel, user_id: window.currentUserId, username: window.currentUsername })); } }); chatForm.addEventListener('submit', async (e) => { e.preventDefault(); const content = chatInput.value.trim(); const file = fileUpload.files[0]; if (!content && !file) return; chatInput.value = ''; const formData = new FormData(); formData.append('content', content); formData.append('channel_id', currentChannel); if (file) { formData.append('file', file); fileUpload.value = ''; // Clear file input } try { const response = await fetch('api_v1_messages.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'message', data: JSON.stringify({ ...result.message, channel_id: currentChannel }) })); } else { appendMessage(result.message); messagesList.scrollTop = messagesList.scrollHeight; } } } catch (err) { console.error('Failed to send message:', err); } }); // Handle Reaction Clicks document.addEventListener('click', (e) => { const badge = e.target.closest('.reaction-badge'); if (badge) { const msgId = badge.parentElement.dataset.messageId; const emoji = badge.dataset.emoji; toggleReaction(msgId, emoji); return; } const addBtn = e.target.closest('.add-reaction-btn'); if (addBtn) { const msgId = addBtn.parentElement.dataset.messageId; showEmojiPicker(addBtn, msgId); return; } // Close picker if click outside if (!e.target.closest('.emoji-picker')) { const picker = document.querySelector('.emoji-picker'); if (picker) picker.remove(); } }); async function toggleReaction(messageId, emoji) { try { const resp = await fetch('api_v1_reactions.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message_id: messageId, emoji: emoji }) }); const result = await resp.json(); if (result.success) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'reaction', message_id: messageId, reactions: result.reactions })); } updateReactionUI(messageId, result.reactions); } } catch (e) { console.error(e); } } function showEmojiPicker(anchor, messageId) { document.querySelector('.emoji-picker')?.remove(); const picker = document.createElement('div'); picker.className = 'emoji-picker'; EMOJIS.forEach(emoji => { const span = document.createElement('span'); span.textContent = emoji; span.onclick = () => { toggleReaction(messageId, emoji); picker.remove(); }; picker.appendChild(span); }); document.body.appendChild(picker); const rect = anchor.getBoundingClientRect(); picker.style.top = `${rect.top - picker.offsetHeight - 5}px`; picker.style.left = `${rect.left}px`; } function updateReactionUI(messageId, reactions) { const container = document.querySelector(`.message-reactions[data-message-id="${messageId}"]`); if (!container) return; const addBtn = container.querySelector('.add-reaction-btn'); container.innerHTML = ''; reactions.forEach(r => { const badge = document.createElement('span'); const userList = r.users.split(','); const active = userList.includes(String(window.currentUserId)); badge.className = `reaction-badge ${active ? 'active' : ''}`; badge.dataset.emoji = r.emoji; badge.innerHTML = `${r.emoji} ${r.count}`; container.appendChild(badge); }); container.appendChild(addBtn); } function updatePresenceUI(userId, status) { const memberItem = document.querySelector(`.start-dm-btn[data-user-id="${userId}"] .message-avatar`); if (memberItem) { let indicator = memberItem.querySelector('.presence-indicator'); if (!indicator) { indicator = document.createElement('div'); indicator.className = 'presence-indicator'; memberItem.appendChild(indicator); } indicator.style.position = 'absolute'; indicator.style.bottom = '0'; indicator.style.right = '0'; indicator.style.width = '10px'; indicator.style.height = '10px'; indicator.style.borderRadius = '50%'; indicator.style.border = '2px solid var(--bg-members)'; indicator.style.backgroundColor = status === 'online' ? '#23a559' : '#80848e'; } } // Voice if (voiceHandler) { document.querySelectorAll('.voice-item').forEach(item => { item.addEventListener('click', () => { const cid = item.dataset.channelId; if (voiceHandler.currentChannelId == cid) { voiceHandler.leave(); item.classList.remove('active'); } else { voiceHandler.join(cid); document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active')); item.classList.add('active'); } }); }); } // Message Actions (Edit/Delete) document.addEventListener('click', async (e) => { const editBtn = e.target.closest('.action-btn.edit'); if (editBtn) { const msgId = editBtn.dataset.id; const msgItem = editBtn.closest('.message-item'); const textEl = msgItem.querySelector('.message-text'); const originalContent = textEl.innerText; const input = document.createElement('input'); input.type = 'text'; input.className = 'form-control bg-dark text-white'; input.value = originalContent; textEl.innerHTML = ''; textEl.appendChild(input); input.focus(); input.onkeydown = async (ev) => { if (ev.key === 'Enter') { const newContent = input.value.trim(); if (newContent && newContent !== originalContent) { const resp = await fetch('api_v1_messages.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: msgId, content: newContent }) }); if ((await resp.json()).success) { textEl.innerHTML = newContent.replace(/\n/g, '
'); ws?.send(JSON.stringify({ type: 'message_edit', message_id: msgId, content: newContent })); } } else { textEl.innerHTML = originalContent.replace(/\n/g, '
'); } } else if (ev.key === 'Escape') { textEl.innerHTML = originalContent.replace(/\n/g, '
'); } }; return; } const deleteBtn = e.target.closest('.action-btn.delete'); if (deleteBtn) { if (!confirm('Delete this message?')) return; const msgId = deleteBtn.dataset.id; const resp = await fetch('api_v1_messages.php', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: msgId }) }); if ((await resp.json()).success) { deleteBtn.closest('.message-item').remove(); ws?.send(JSON.stringify({ type: 'message_delete', message_id: msgId })); } return; } // Start DM const dmBtn = e.target.closest('.start-dm-btn'); if (dmBtn) { const userId = dmBtn.dataset.userId; const formData = new FormData(); formData.append('user_id', userId); const resp = await fetch('api_v1_dms.php', { method: 'POST', body: formData }); const result = await resp.json(); if (result.success) { window.location.href = `?server_id=dms&channel_id=${result.channel_id}`; } } }); // Global Search const searchInput = document.getElementById('global-search'); const searchResults = document.getElementById('search-results'); searchInput.addEventListener('input', async () => { const q = searchInput.value.trim(); if (q.length < 2) { searchResults.style.display = 'none'; return; } const resp = await fetch(`api_v1_search.php?q=${encodeURIComponent(q)}&channel_id=${currentChannel}`); const data = await resp.json(); if (data.success && data.results.length > 0) { searchResults.innerHTML = ''; data.results.forEach(res => { const item = document.createElement('div'); item.className = 'search-result-item'; item.innerHTML = `
${res.username}
${res.content}
`; item.onclick = () => { // Logic to scroll to message would go here searchResults.style.display = 'none'; }; searchResults.appendChild(item); }); searchResults.style.display = 'block'; } else { searchResults.innerHTML = '
No results found
'; searchResults.style.display = 'block'; } }); document.addEventListener('click', (e) => { if (!e.target.closest('.search-container')) { searchResults.style.display = 'none'; } }); // Roles Management const rolesTabBtn = document.getElementById('roles-tab-btn'); const rolesList = document.getElementById('roles-list'); const addRoleBtn = document.getElementById('add-role-btn'); const activeServerId = new URLSearchParams(window.location.search).get('server_id') || 1; rolesTabBtn?.addEventListener('click', loadRoles); async function loadRoles() { rolesList.innerHTML = '
Loading roles...
'; try { const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`); const data = await resp.json(); if (data.success) { renderRoles(data.roles); } } catch (e) { console.error(e); } } function renderRoles(roles) { rolesList.innerHTML = ''; roles.forEach(role => { const item = document.createElement('div'); item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2'; item.innerHTML = `
${role.name}
`; rolesList.appendChild(item); }); } addRoleBtn?.addEventListener('click', async () => { const name = prompt('Role name:'); if (!name) return; const color = prompt('Role color (hex):', '#99aab5'); try { const resp = await fetch('api_v1_roles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'create', server_id: activeServerId, name, color }) }); if ((await resp.json()).success) loadRoles(); } catch (e) { console.error(e); } }); rolesList?.addEventListener('click', async (e) => { if (e.target.classList.contains('delete-role-btn')) { if (!confirm('Delete this role?')) return; const roleId = e.target.dataset.id; const resp = await fetch('api_v1_roles.php', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: roleId }) }); if ((await resp.json()).success) loadRoles(); } if (e.target.classList.contains('edit-role-btn')) { const roleId = e.target.dataset.id; const name = prompt('New name:'); const color = prompt('New color (hex):'); if (!name || !color) return; const resp = await fetch('api_v1_roles.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: roleId, name, color, permissions: 0 }) }); if ((await resp.json()).success) loadRoles(); } }); // Server Settings const searchServerIconBtn = document.getElementById('search-server-icon-btn'); const serverIconResults = document.getElementById('server-icon-search-results'); const serverIconPreview = document.getElementById('server-icon-preview'); const serverIconUrlInput = document.getElementById('server-icon-url'); searchServerIconBtn?.addEventListener('click', async () => { const query = prompt('Search for a server icon:', 'abstract'); if (!query) return; serverIconResults.innerHTML = '
Searching...
'; try { const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(query)}`); const data = await resp.json(); serverIconResults.innerHTML = ''; data.forEach(photo => { const img = document.createElement('img'); img.src = photo.url; img.className = 'avatar-pick'; img.style.width = '50px'; img.style.height = '50px'; img.onclick = () => { serverIconUrlInput.value = photo.url; serverIconPreview.style.backgroundImage = `url('${photo.url}')`; serverIconResults.innerHTML = ''; }; serverIconResults.appendChild(img); }); } catch (e) { serverIconResults.innerHTML = '
Error fetching icons
'; } }); }); function appendMessage(msg) { const messagesList = document.getElementById('messages-list'); const div = document.createElement('div'); div.className = 'message-item'; div.dataset.id = msg.id; const avatarStyle = msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : ''; let attachmentHtml = ''; if (msg.attachment_url) { const ext = msg.attachment_url.split('.').pop().toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) { attachmentHtml = `
Attachment
`; } else { attachmentHtml = `
${msg.attachment_url.split('/').pop()}
`; } } let embedHtml = ''; if (msg.metadata) { const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata; embedHtml = `
${meta.site_name ? `
${meta.site_name}
` : ''} ${meta.title ? `${meta.title}` : ''} ${meta.description ? `
${meta.description}
` : ''} ${meta.image ? `
` : ''}
`; } const isMe = msg.user_id == window.currentUserId || msg.username == window.currentUsername; const actionsHtml = isMe ? `
` : ''; div.innerHTML = `
${msg.username} ${msg.time} ${actionsHtml}
${msg.content.replace(/\n/g, '
')} ${attachmentHtml} ${embedHtml}
+
`; messagesList.appendChild(div); }