541 lines
23 KiB
JavaScript
541 lines
23 KiB
JavaScript
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, '<br>');
|
||
} 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} <span class="count">${r.count}</span>`;
|
||
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, '<br>');
|
||
ws?.send(JSON.stringify({ type: 'message_edit', message_id: msgId, content: newContent }));
|
||
}
|
||
} else {
|
||
textEl.innerHTML = originalContent.replace(/\n/g, '<br>');
|
||
}
|
||
} else if (ev.key === 'Escape') {
|
||
textEl.innerHTML = originalContent.replace(/\n/g, '<br>');
|
||
}
|
||
};
|
||
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 = `
|
||
<div class="search-result-author">${res.username}</div>
|
||
<div class="search-result-text">${res.content}</div>
|
||
`;
|
||
item.onclick = () => {
|
||
// Logic to scroll to message would go here
|
||
searchResults.style.display = 'none';
|
||
};
|
||
searchResults.appendChild(item);
|
||
});
|
||
searchResults.style.display = 'block';
|
||
} else {
|
||
searchResults.innerHTML = '<div class="p-2 text-muted">No results found</div>';
|
||
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 = '<div class="text-center p-3 text-muted">Loading roles...</div>';
|
||
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 = `
|
||
<div class="d-flex align-items-center">
|
||
<div style="width: 12px; height: 12px; border-radius: 50%; background-color: ${role.color}; margin-right: 10px;"></div>
|
||
<span>${role.name}</span>
|
||
</div>
|
||
<div>
|
||
<button class="btn btn-sm btn-outline-light edit-role-btn" data-id="${role.id}">Edit</button>
|
||
<button class="btn btn-sm btn-outline-danger delete-role-btn" data-id="${role.id}">×</button>
|
||
</div>
|
||
`;
|
||
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 = '<div class="text-muted small">Searching...</div>';
|
||
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 = '<div class="text-danger small">Error fetching icons</div>';
|
||
}
|
||
});
|
||
});
|
||
|
||
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 = `<div class="message-attachment mt-2"><img src="${msg.attachment_url}" class="img-fluid rounded message-img-preview" alt="Attachment" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)"></div>`;
|
||
} else {
|
||
attachmentHtml = `<div class="message-attachment mt-2"><a href="${msg.attachment_url}" target="_blank" class="attachment-link d-inline-flex align-items-center p-2 rounded bg-dark text-white text-decoration-none"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>${msg.attachment_url.split('/').pop()}</a></div>`;
|
||
}
|
||
}
|
||
|
||
let embedHtml = '';
|
||
if (msg.metadata) {
|
||
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata;
|
||
embedHtml = `
|
||
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid var(--blurple); max-width: 520px;">
|
||
${meta.site_name ? `<div class="embed-site-name mb-1" style="font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; font-weight: bold;">${meta.site_name}</div>` : ''}
|
||
${meta.title ? `<a href="${meta.url}" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc;">${meta.title}</a>` : ''}
|
||
${meta.description ? `<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);">${meta.description}</div>` : ''}
|
||
${meta.image ? `<div class="embed-image"><img src="${meta.image}" class="rounded" style="max-width: 100%; max-height: 300px; object-fit: contain;"></div>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const isMe = msg.user_id == window.currentUserId || msg.username == window.currentUsername;
|
||
const actionsHtml = isMe ? `
|
||
<div class="message-actions-menu">
|
||
<span class="action-btn edit" title="Edit" data-id="${msg.id}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
|
||
</span>
|
||
<span class="action-btn delete" title="Delete" data-id="${msg.id}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
||
</span>
|
||
</div>
|
||
` : '';
|
||
|
||
div.innerHTML = `
|
||
<div class="message-avatar" style="${avatarStyle}"></div>
|
||
<div class="message-content">
|
||
<div class="message-author">
|
||
${msg.username}
|
||
<span class="message-time">${msg.time}</span>
|
||
${actionsHtml}
|
||
</div>
|
||
<div class="message-text">
|
||
${msg.content.replace(/\n/g, '<br>')}
|
||
${attachmentHtml}
|
||
${embedHtml}
|
||
</div>
|
||
<div class="message-reactions mt-1" data-message-id="${msg.id}">
|
||
<span class="add-reaction-btn" title="Add Reaction">+</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
messagesList.appendChild(div);
|
||
}
|