38443-vm/assets/js/main.js
2026-02-15 10:55:02 +00:00

541 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}