1492 lines
68 KiB
JavaScript
1492 lines
68 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;
|
||
const currentThread = new URLSearchParams(window.location.search).get('thread_id');
|
||
let typingTimeout;
|
||
|
||
// Notification Permission
|
||
if ("Notification" in window && Notification.permission === "default") {
|
||
Notification.requestPermission();
|
||
}
|
||
|
||
// 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;
|
||
|
||
// Desktop Notifications for mentions
|
||
if (data.content.includes(`@${window.currentUsername}`) && data.user_id != window.currentUserId) {
|
||
if (Notification.permission === "granted" && !window.isDndMode) {
|
||
new Notification(`Mention in #${window.currentChannelName}`, {
|
||
body: `${data.username}: ${data.content}`,
|
||
icon: data.avatar_url || ''
|
||
});
|
||
}
|
||
}
|
||
}
|
||
} 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', (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 (currentThread) {
|
||
formData.append('thread_id', currentThread);
|
||
}
|
||
|
||
const progressContainer = document.getElementById('upload-progress-container');
|
||
const progressBar = document.getElementById('upload-progress-bar');
|
||
const progressPercent = document.getElementById('upload-percentage');
|
||
const progressFilename = document.getElementById('upload-filename');
|
||
|
||
if (file) {
|
||
formData.append('file', file);
|
||
fileUpload.value = ''; // Clear file input
|
||
|
||
// Show progress bar
|
||
progressContainer.style.display = 'block';
|
||
progressFilename.textContent = `Uploading: ${file.name}`;
|
||
progressBar.style.width = '0%';
|
||
progressPercent.textContent = '0%';
|
||
}
|
||
|
||
const xhr = new XMLHttpRequest();
|
||
xhr.open('POST', 'api_v1_messages.php', true);
|
||
|
||
xhr.upload.onprogress = (ev) => {
|
||
if (ev.lengthComputable && file) {
|
||
const percent = Math.round((ev.loaded / ev.total) * 100);
|
||
progressBar.style.width = percent + '%';
|
||
progressPercent.textContent = percent + '%';
|
||
}
|
||
};
|
||
|
||
xhr.onload = () => {
|
||
if (xhr.status === 200) {
|
||
const result = JSON.parse(xhr.responseText);
|
||
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;
|
||
}
|
||
} else {
|
||
alert(result.error || 'Failed to send message');
|
||
}
|
||
}
|
||
progressContainer.style.display = 'none';
|
||
};
|
||
|
||
xhr.onerror = () => {
|
||
console.error('XHR Error');
|
||
progressContainer.style.display = 'none';
|
||
alert('An error occurred during the upload.');
|
||
};
|
||
|
||
xhr.send(formData);
|
||
});
|
||
|
||
// 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;
|
||
}
|
||
|
||
const pinBtn = e.target.closest('.action-btn.pin');
|
||
if (pinBtn) {
|
||
const msgId = pinBtn.dataset.id;
|
||
const isPinned = pinBtn.dataset.pinned == '1';
|
||
const action = isPinned ? 'unpin' : 'pin';
|
||
|
||
const resp = await fetch('api_v1_messages.php', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: msgId, action: action })
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload(); // Simplest way to reflect changes across UI
|
||
}
|
||
return;
|
||
}
|
||
|
||
const pinnedMessagesBtn = document.getElementById('pinned-messages-btn');
|
||
if (e.target.closest('#pinned-messages-btn')) {
|
||
const container = document.getElementById('pinned-messages-container');
|
||
container.innerHTML = '<div class="p-3 text-center text-muted">Loading pinned messages...</div>';
|
||
const modal = new bootstrap.Modal(document.getElementById('pinnedMessagesModal'));
|
||
modal.show();
|
||
|
||
const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&pinned=1`);
|
||
const data = await resp.json();
|
||
if (data.success && data.messages.length > 0) {
|
||
container.innerHTML = '';
|
||
data.messages.forEach(msg => {
|
||
const div = document.createElement('div');
|
||
div.className = 'message-item p-2 border-bottom border-secondary';
|
||
div.style.backgroundColor = 'transparent';
|
||
const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
|
||
div.innerHTML = `
|
||
<div class="d-flex align-items-start">
|
||
<div class="message-avatar" style="width: 32px; height: 32px; margin-right: 10px; ${msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : ''}"></div>
|
||
<div style="flex: 1;">
|
||
<div class="message-author" style="font-size: 0.85em; ${authorStyle}">
|
||
${escapeHTML(msg.username)}
|
||
<span class="message-time">${msg.time}</span>
|
||
</div>
|
||
<div class="message-text" style="font-size: 0.9em;">
|
||
${escapeHTML(msg.content).replace(/\n/g, '<br>')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
container.appendChild(div);
|
||
});
|
||
} else {
|
||
container.innerHTML = '<div class="p-3 text-center text-muted">No pinned messages in this channel.</div>';
|
||
}
|
||
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 searchType = document.getElementById('search-type');
|
||
const searchResults = document.getElementById('search-results');
|
||
|
||
searchInput?.addEventListener('input', async () => {
|
||
const q = searchInput.value.trim();
|
||
const type = searchType.value;
|
||
if (q.length < 2) {
|
||
searchResults.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const resp = await fetch(`api_v1_search.php?q=${encodeURIComponent(q)}&type=${type}&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 d-flex align-items-center gap-2';
|
||
if (type === 'users') {
|
||
item.innerHTML = `
|
||
<div class="message-avatar" style="width: 24px; height: 24px; ${res.avatar_url ? `background-image: url('${res.avatar_url}');` : ''}"></div>
|
||
<div class="flex-grow-1">
|
||
<div class="search-result-author">${res.username}</div>
|
||
<div class="small text-muted" style="font-size: 0.7em;">Click to start conversation</div>
|
||
</div>
|
||
`;
|
||
item.onclick = () => {
|
||
const formData = new FormData();
|
||
formData.append('user_id', res.id);
|
||
fetch('api_v1_dms.php', { method: 'POST', body: formData })
|
||
.then(r => r.json())
|
||
.then(resDM => {
|
||
if (resDM.success) window.location.href = `?server_id=dms&channel_id=${resDM.channel_id}`;
|
||
});
|
||
};
|
||
} else {
|
||
item.innerHTML = `
|
||
<div class="flex-grow-1">
|
||
<div class="search-result-author">${res.username}</div>
|
||
<div class="search-result-text">${res.content}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
searchResults.appendChild(item);
|
||
});
|
||
searchResults.style.display = 'block';
|
||
} else {
|
||
searchResults.innerHTML = '<div class="p-2 text-muted">No results found</div>';
|
||
searchResults.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
// Channel Permissions Management
|
||
const channelPermissionsTabBtn = document.getElementById('channel-permissions-tab-btn');
|
||
const channelPermissionsList = document.getElementById('channel-permissions-list');
|
||
const addPermRoleList = document.getElementById('add-permission-role-list');
|
||
|
||
channelPermissionsTabBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
loadChannelPermissions(channelId);
|
||
loadRolesForPermissions(channelId);
|
||
});
|
||
|
||
async function loadChannelPermissions(channelId) {
|
||
channelPermissionsList.innerHTML = '<div class="text-center p-3 text-muted small">Loading permissions...</div>';
|
||
const resp = await fetch(`api_v1_channel_permissions.php?channel_id=${channelId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
renderChannelPermissions(channelId, data.permissions);
|
||
}
|
||
}
|
||
|
||
async function loadRolesForPermissions(channelId) {
|
||
addPermRoleList.innerHTML = '';
|
||
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
data.roles.forEach(role => {
|
||
const li = document.createElement('li');
|
||
li.innerHTML = `<a class="dropdown-item d-flex align-items-center gap-2" href="#">
|
||
<div style="width: 10px; height: 10px; border-radius: 50%; background-color: ${role.color};"></div>
|
||
${role.name}
|
||
</a>`;
|
||
li.onclick = async (e) => {
|
||
e.preventDefault();
|
||
await fetch('api_v1_channel_permissions.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ channel_id: channelId, role_id: role.id, allow: 0, deny: 0 })
|
||
});
|
||
loadChannelPermissions(channelId);
|
||
};
|
||
addPermRoleList.appendChild(li);
|
||
});
|
||
}
|
||
}
|
||
|
||
function renderChannelPermissions(channelId, permissions) {
|
||
channelPermissionsList.innerHTML = '';
|
||
if (permissions.length === 0) {
|
||
channelPermissionsList.innerHTML = '<div class="text-center p-3 text-muted small">No role overrides.</div>';
|
||
return;
|
||
}
|
||
permissions.forEach(p => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary p-2';
|
||
item.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<div class="d-flex align-items-center">
|
||
<div style="width: 10px; height: 10px; border-radius: 50%; background-color: ${p.role_color}; margin-right: 8px;"></div>
|
||
<span class="small fw-bold">${p.role_name}</span>
|
||
</div>
|
||
<button class="btn btn-sm text-danger remove-perm-btn" data-role-id="${p.role_id}">×</button>
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
<select class="form-select form-select-sm bg-dark text-white border-secondary perm-select" data-role-id="${p.role_id}">
|
||
<option value="allow" ${p.allow_permissions ? 'selected' : ''}>Allow Sending Messages</option>
|
||
<option value="deny" ${p.deny_permissions ? 'selected' : ''}>Deny Sending Messages</option>
|
||
<option value="neutral" ${!p.allow_permissions && !p.deny_permissions ? 'selected' : ''}>Neutral</option>
|
||
</select>
|
||
</div>
|
||
`;
|
||
channelPermissionsList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
channelPermissionsList?.addEventListener('click', async (e) => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
if (e.target.classList.contains('remove-perm-btn')) {
|
||
const roleId = e.target.dataset.roleId;
|
||
await fetch('api_v1_channel_permissions.php', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ channel_id: channelId, role_id: roleId })
|
||
});
|
||
loadChannelPermissions(channelId);
|
||
}
|
||
});
|
||
|
||
channelPermissionsList?.addEventListener('change', async (e) => {
|
||
if (e.target.classList.contains('perm-select')) {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
const roleId = e.target.dataset.roleId;
|
||
const val = e.target.value;
|
||
let allow = 0, deny = 0;
|
||
if (val === 'allow') allow = 1;
|
||
if (val === 'deny') deny = 1;
|
||
|
||
await fetch('api_v1_channel_permissions.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ channel_id: channelId, role_id: roleId, allow, deny })
|
||
});
|
||
}
|
||
});
|
||
|
||
document.addEventListener('click', async (e) => {
|
||
if (!e.target.closest('.search-container')) {
|
||
searchResults.style.display = 'none';
|
||
}
|
||
|
||
if (e.target.classList.contains('move-rule-btn')) {
|
||
const id = e.target.dataset.id;
|
||
const dir = e.target.dataset.dir;
|
||
const resp = await fetch('api_v1_rules.php', {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id, dir })
|
||
});
|
||
if ((await resp.json()).success) location.reload();
|
||
}
|
||
});
|
||
|
||
// Roles Management
|
||
const channelSettingsBtns = document.querySelectorAll('.channel-settings-btn');
|
||
channelSettingsBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const modal = document.getElementById('editChannelModal');
|
||
const channelId = btn.dataset.id;
|
||
const channelType = btn.dataset.type || 'chat';
|
||
|
||
modal.querySelector('#edit-channel-id').value = channelId;
|
||
modal.querySelector('#edit-channel-name').value = btn.dataset.name;
|
||
modal.querySelector('#edit-channel-type').value = channelType;
|
||
modal.querySelector('#edit-channel-files').checked = btn.dataset.files == '1';
|
||
modal.querySelector('#edit-channel-limit').value = btn.dataset.limit || '';
|
||
modal.querySelector('#edit-channel-status').value = btn.dataset.status || '';
|
||
modal.querySelector('#edit-channel-theme').value = btn.dataset.theme || '#5865f2';
|
||
modal.querySelector('#edit-channel-icon').value = btn.dataset.icon || '';
|
||
modal.querySelector('#delete-channel-id').value = channelId;
|
||
|
||
// Show/Hide RSS tab
|
||
const rssTabNav = document.getElementById('rss-tab-nav');
|
||
const statusContainer = document.getElementById('edit-channel-status-container');
|
||
|
||
if (channelType === 'announcement') {
|
||
rssTabNav.style.display = 'block';
|
||
} else {
|
||
rssTabNav.style.display = 'none';
|
||
// Switch to General tab if we were on RSS
|
||
if (document.getElementById('rss-tab-btn').classList.contains('active')) {
|
||
bootstrap.Tab.getInstance(modal.querySelector('.nav-link.active')).hide();
|
||
bootstrap.Tab.getOrCreateInstance(modal.querySelector('[data-bs-target="#edit-channel-general"]')).show();
|
||
}
|
||
}
|
||
|
||
if (channelType === 'voice') {
|
||
statusContainer.style.display = 'block';
|
||
} else {
|
||
statusContainer.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
|
||
// RSS Management
|
||
const editChannelType = document.getElementById('edit-channel-type');
|
||
editChannelType?.addEventListener('change', () => {
|
||
const type = editChannelType.value;
|
||
const rssTabNav = document.getElementById('rss-tab-nav');
|
||
const statusContainer = document.getElementById('edit-channel-status-container');
|
||
|
||
rssTabNav.style.display = (type === 'announcement') ? 'block' : 'none';
|
||
statusContainer.style.display = (type === 'voice') ? 'block' : 'none';
|
||
});
|
||
|
||
// RSS Management
|
||
const rssTabBtn = document.getElementById('rss-tab-btn');
|
||
const rssFeedsList = document.getElementById('rss-feeds-list');
|
||
const addRssBtn = document.getElementById('add-rss-btn');
|
||
const syncRssBtn = document.getElementById('sync-rss-btn');
|
||
|
||
rssTabBtn?.addEventListener('click', loadRssFeeds);
|
||
|
||
async function loadRssFeeds() {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
rssFeedsList.innerHTML = '<div class="text-center p-3 text-muted small">Loading feeds...</div>';
|
||
try {
|
||
const resp = await fetch(`api_v1_rss.php?channel_id=${channelId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
renderRssFeeds(data.feeds);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function renderRssFeeds(feeds) {
|
||
rssFeedsList.innerHTML = '';
|
||
if (feeds.length === 0) {
|
||
rssFeedsList.innerHTML = '<div class="text-center p-3 text-muted small">No RSS feeds configured.</div>';
|
||
return;
|
||
}
|
||
feeds.forEach(feed => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-1';
|
||
item.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||
<span class="small text-truncate" style="max-width: 80%;">${feed.url}</span>
|
||
<button class="btn btn-sm text-danger delete-rss-btn" data-id="${feed.id}">×</button>
|
||
</div>
|
||
<div class="small text-muted" style="font-size: 0.7em;">Last fetched: ${feed.last_fetched_at || 'Never'}</div>
|
||
`;
|
||
rssFeedsList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
addRssBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
const url = document.getElementById('new-rss-url').value.trim();
|
||
if (!url) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('action', 'add');
|
||
formData.append('channel_id', channelId);
|
||
formData.append('url', url);
|
||
|
||
const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData });
|
||
if ((await resp.json()).success) {
|
||
document.getElementById('new-rss-url').value = '';
|
||
loadRssFeeds();
|
||
}
|
||
});
|
||
|
||
syncRssBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
syncRssBtn.disabled = true;
|
||
syncRssBtn.textContent = 'Syncing...';
|
||
|
||
const formData = new FormData();
|
||
formData.append('action', 'sync');
|
||
formData.append('channel_id', channelId);
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData });
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
alert(`Sync complete! Found ${result.new_items} new items.`);
|
||
loadRssFeeds();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
|
||
syncRssBtn.disabled = false;
|
||
syncRssBtn.textContent = 'Sync Now';
|
||
});
|
||
|
||
rssFeedsList?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-rss-btn')) {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
const feedId = e.target.dataset.id;
|
||
const formData = new FormData();
|
||
formData.append('action', 'delete');
|
||
formData.append('channel_id', channelId);
|
||
formData.append('feed_id', feedId);
|
||
|
||
await fetch('api_v1_rss.php', { method: 'POST', body: formData });
|
||
loadRssFeeds();
|
||
}
|
||
});
|
||
|
||
// Clear Channel History
|
||
const clearHistoryBtn = document.getElementById('clear-channel-history-btn');
|
||
clearHistoryBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
if (!confirm('Voulez-vous vraiment vider tout l\'historique de ce salon ? Cette action est irréversible.')) return;
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('channel_id', channelId);
|
||
const resp = await fetch('api_v1_clear_channel.php', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(result.error || 'Erreur lors du nettoyage de l\'historique');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Roles Management
|
||
const rolesTabBtn = document.getElementById('roles-tab-btn');
|
||
const rolesList = document.getElementById('roles-list');
|
||
const addRoleBtn = document.getElementById('add-role-btn');
|
||
const membersTabBtn = document.getElementById('members-tab-btn');
|
||
const membersList = document.getElementById('server-members-list');
|
||
const activeServerId = new URLSearchParams(window.location.search).get('server_id') || 1;
|
||
|
||
let serverRoles = [];
|
||
let serverPermissions = [];
|
||
|
||
rolesTabBtn?.addEventListener('click', loadRoles);
|
||
membersTabBtn?.addEventListener('click', loadRoles); // Both tabs need roles data
|
||
|
||
async function loadRoles() {
|
||
if (rolesList) rolesList.innerHTML = '<div class="text-center p-3 text-muted">Loading...</div>';
|
||
if (membersList) membersList.innerHTML = '<div class="text-center p-3 text-muted">Loading...</div>';
|
||
|
||
try {
|
||
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
serverRoles = data.roles;
|
||
serverPermissions = data.permissions_list;
|
||
if (rolesList) renderRoles(data.roles);
|
||
if (membersList) renderMembers(data.members);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function renderRoles(roles) {
|
||
rolesList.innerHTML = '';
|
||
if (roles.length === 0) {
|
||
rolesList.innerHTML = '<div class="text-center p-3 text-muted">No roles created yet.</div>';
|
||
}
|
||
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 mb-1 rounded';
|
||
item.innerHTML = `
|
||
<div class="d-flex align-items-center">
|
||
<div style="width: 14px; height: 14px; border-radius: 50%; background-color: ${role.color}; margin-right: 12px; box-shadow: 0 0 5px ${role.color}88;"></div>
|
||
<span class="fw-medium">${role.name}</span>
|
||
</div>
|
||
<div>
|
||
<button class="btn btn-sm btn-outline-light edit-role-btn-v2" data-id="${role.id}" data-name="${role.name}" data-color="${role.color}" data-perms="${role.permissions}">Edit</button>
|
||
<button class="btn btn-sm btn-outline-danger delete-role-btn" data-id="${role.id}">×</button>
|
||
</div>
|
||
`;
|
||
rolesList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
function renderMembers(members) {
|
||
membersList.innerHTML = '';
|
||
members.forEach(member => {
|
||
const memberRoles = member.role_ids ? member.role_ids.split(',') : [];
|
||
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 mb-2 rounded bg-dark';
|
||
|
||
let rolesHtml = '';
|
||
serverRoles.forEach(role => {
|
||
const isAssigned = memberRoles.includes(role.id.toString());
|
||
rolesHtml += `
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input role-assign-check" type="checkbox"
|
||
data-user-id="${member.id}" data-role-id="${role.id}"
|
||
${isAssigned ? 'checked' : ''}>
|
||
<label class="form-check-label small" style="color: ${role.color}">${role.name}</label>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
item.innerHTML = `
|
||
<div class="d-flex align-items-center flex-grow-1">
|
||
<div class="message-avatar me-2" style="width: 32px; height: 32px; ${member.avatar_url ? `background-image: url('${member.avatar_url}');` : ''}"></div>
|
||
<div class="flex-grow-1">
|
||
<div class="fw-bold small" style="color: ${member.role_color || 'inherit'}">${member.username}</div>
|
||
<div class="member-roles-assign-list">
|
||
${rolesHtml || '<span class="text-muted small">No roles available</span>'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
membersList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
// Role Editing Modal Logic
|
||
rolesList?.addEventListener('click', (e) => {
|
||
if (e.target.classList.contains('edit-role-btn-v2')) {
|
||
const role = e.target.dataset;
|
||
document.getElementById('edit-role-id').value = role.id;
|
||
document.getElementById('edit-role-name').value = role.name;
|
||
document.getElementById('edit-role-color').value = role.color;
|
||
|
||
const permsContainer = document.getElementById('role-permissions-checkboxes');
|
||
permsContainer.innerHTML = '';
|
||
const currentPerms = parseInt(role.perms);
|
||
|
||
serverPermissions.forEach(p => {
|
||
const isChecked = (currentPerms & p.value) === p.value;
|
||
permsContainer.innerHTML += `
|
||
<div class="form-check mb-1">
|
||
<input class="form-check-input perm-check" type="checkbox" value="${p.value}" id="perm-${p.value}" ${isChecked ? 'checked' : ''}>
|
||
<label class="form-check-label text-white small" for="perm-${p.value}">${p.name}</label>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('roleEditorModal'));
|
||
modal.show();
|
||
}
|
||
});
|
||
|
||
document.getElementById('save-role-btn')?.addEventListener('click', async () => {
|
||
const id = document.getElementById('edit-role-id').value;
|
||
const name = document.getElementById('edit-role-name').value;
|
||
const color = document.getElementById('edit-role-color').value;
|
||
|
||
let permissions = 0;
|
||
document.querySelectorAll('.perm-check:checked').forEach(cb => {
|
||
permissions |= parseInt(cb.value);
|
||
});
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_roles.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'update', server_id: activeServerId, id, name, color, permissions })
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
bootstrap.Modal.getInstance(document.getElementById('roleEditorModal')).hide();
|
||
loadRoles();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
membersList?.addEventListener('change', async (e) => {
|
||
if (e.target.classList.contains('role-assign-check')) {
|
||
const userId = e.target.dataset.userId;
|
||
const roleId = e.target.dataset.roleId;
|
||
const action = e.target.checked ? 'assign' : 'unassign';
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_roles.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action, server_id: activeServerId, user_id: userId, role_id: roleId })
|
||
});
|
||
const data = await resp.json();
|
||
if (!data.success) {
|
||
alert(data.error || 'Failed to update role');
|
||
e.target.checked = !e.target.checked;
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
});
|
||
|
||
addRoleBtn?.addEventListener('click', async () => {
|
||
const name = prompt('Role name:');
|
||
if (!name) return;
|
||
const color = '#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, permissions: 0 })
|
||
});
|
||
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: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', server_id: activeServerId, id: roleId })
|
||
});
|
||
if ((await resp.json()).success) loadRoles();
|
||
}
|
||
});
|
||
|
||
// Webhooks Management
|
||
const webhooksTabBtn = document.getElementById('webhooks-tab-btn');
|
||
const webhooksList = document.getElementById('webhooks-list');
|
||
const addWebhookBtn = document.getElementById('add-webhook-btn');
|
||
|
||
webhooksTabBtn?.addEventListener('click', loadWebhooks);
|
||
|
||
async function loadWebhooks() {
|
||
webhooksList.innerHTML = '<div class="text-center p-3 text-muted">Loading webhooks...</div>';
|
||
try {
|
||
const resp = await fetch(`api_v1_webhook.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
renderWebhooks(data.webhooks);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function renderWebhooks(webhooks) {
|
||
webhooksList.innerHTML = '';
|
||
if (webhooks.length === 0) {
|
||
webhooksList.innerHTML = '<div class="text-center p-3 text-muted">No webhooks found.</div>';
|
||
return;
|
||
}
|
||
webhooks.forEach(wh => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-2';
|
||
const url = `${window.location.origin}/api_v1_webhook.php?token=${wh.token}`;
|
||
item.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||
<span class="fw-bold">${wh.name}</span>
|
||
<button class="btn btn-sm btn-outline-danger delete-webhook-btn" data-id="${wh.id}">×</button>
|
||
</div>
|
||
<div class="small text-muted mb-2">Channel: #${wh.channel_name}</div>
|
||
<div class="input-group input-group-sm">
|
||
<input type="text" class="form-control bg-dark text-white border-secondary" value="${url}" readonly>
|
||
<button class="btn btn-outline-secondary" type="button" onclick="navigator.clipboard.writeText('${url}')">Copy</button>
|
||
</div>
|
||
`;
|
||
webhooksList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
addWebhookBtn?.addEventListener('click', async () => {
|
||
const name = prompt('Webhook name:', 'Bot Name');
|
||
if (!name) return;
|
||
|
||
// Fetch channels for this server to let user pick one
|
||
const respChannels = await fetch(`api_v1_channels.php?server_id=${activeServerId}`);
|
||
const dataChannels = await respChannels.json();
|
||
if (!dataChannels.length) return alert('Create a channel first.');
|
||
|
||
const channelId = prompt('Enter Channel ID:\n' + dataChannels.map(c => `${c.id}: #${c.name}`).join('\n'));
|
||
if (!channelId) return;
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_webhook.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ channel_id: channelId, name: name })
|
||
});
|
||
if ((await resp.json()).success) loadWebhooks();
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
webhooksList?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-webhook-btn')) {
|
||
if (!confirm('Delete this webhook?')) return;
|
||
const whId = e.target.dataset.id;
|
||
const resp = await fetch('api_v1_webhook.php', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: whId })
|
||
});
|
||
if ((await resp.json()).success) loadWebhooks();
|
||
}
|
||
});
|
||
|
||
// Stats Management
|
||
const statsTabBtn = document.getElementById('stats-tab-btn');
|
||
statsTabBtn?.addEventListener('click', loadStats);
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const resp = await fetch(`api_v1_stats.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
document.getElementById('stat-members').textContent = data.stats.total_members;
|
||
document.getElementById('stat-messages').textContent = data.stats.total_messages;
|
||
|
||
const topUsersList = document.getElementById('top-users-list');
|
||
topUsersList.innerHTML = '';
|
||
data.stats.top_users.forEach(user => {
|
||
const item = document.createElement('div');
|
||
item.className = 'd-flex justify-content-between align-items-center mb-1 p-2 bg-dark rounded';
|
||
item.innerHTML = `<span>${user.username}</span><span class="badge bg-primary">${user.message_count} msgs</span>`;
|
||
topUsersList.appendChild(item);
|
||
});
|
||
|
||
const activity = document.getElementById('activity-chart-placeholder');
|
||
activity.innerHTML = '';
|
||
data.stats.history.forEach(day => {
|
||
const bar = document.createElement('div');
|
||
bar.className = 'd-flex align-items-center mb-1';
|
||
const percent = Math.min(100, (day.count / 100) * 100); // Normalize to 100 for visual
|
||
bar.innerHTML = `
|
||
<div style="width: 80px;" class="small">${day.date}</div>
|
||
<div class="flex-grow-1 mx-2" style="height: 10px; background: #1e1f22; border-radius: 5px;">
|
||
<div style="width: ${percent}%; height: 100%; background: var(--blurple); border-radius: 5px;"></div>
|
||
</div>
|
||
<div style="width: 30px;" class="small text-end">${day.count}</div>
|
||
`;
|
||
activity.appendChild(bar);
|
||
});
|
||
if (data.stats.history.length === 0) {
|
||
activity.innerHTML = '<div class="text-muted">No activity in the last 7 days.</div>';
|
||
}
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
// 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>';
|
||
}
|
||
});
|
||
|
||
// Forum: New Thread
|
||
const newThreadBtn = document.getElementById('new-thread-btn');
|
||
const newThreadModal = document.getElementById('newThreadModal') ? new bootstrap.Modal(document.getElementById('newThreadModal')) : null;
|
||
let selectedTagIds = [];
|
||
|
||
newThreadBtn?.addEventListener('click', async () => {
|
||
if (!newThreadModal) return;
|
||
|
||
// Load tags for this channel
|
||
const tagsList = document.getElementById('new-thread-tags-list');
|
||
tagsList.innerHTML = '<div class="text-muted small">Loading tags...</div>';
|
||
selectedTagIds = [];
|
||
|
||
try {
|
||
const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`);
|
||
const data = await resp.json();
|
||
tagsList.innerHTML = '';
|
||
if (data.success && data.tags.length > 0) {
|
||
data.tags.forEach(tag => {
|
||
const span = document.createElement('span');
|
||
span.className = 'badge rounded-pill p-2 border border-secondary';
|
||
span.style.cursor = 'pointer';
|
||
span.style.backgroundColor = 'transparent';
|
||
span.dataset.id = tag.id;
|
||
span.dataset.color = tag.color;
|
||
span.textContent = tag.name;
|
||
span.onclick = () => {
|
||
if (selectedTagIds.includes(tag.id)) {
|
||
selectedTagIds = selectedTagIds.filter(id => id !== tag.id);
|
||
span.style.backgroundColor = 'transparent';
|
||
} else {
|
||
selectedTagIds.push(tag.id);
|
||
span.style.backgroundColor = tag.color;
|
||
}
|
||
};
|
||
tagsList.appendChild(span);
|
||
});
|
||
} else {
|
||
tagsList.innerHTML = '<div class="text-muted small">No tags available.</div>';
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
|
||
newThreadModal.show();
|
||
});
|
||
|
||
document.getElementById('submit-new-thread-btn')?.addEventListener('click', async () => {
|
||
const title = document.getElementById('new-thread-title').value.trim();
|
||
if (!title) return;
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('channel_id', currentChannel);
|
||
formData.append('title', title);
|
||
formData.append('tag_ids', selectedTagIds.join(','));
|
||
const resp = await fetch('api_v1_threads.php', { method: 'POST', body: formData });
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
window.location.href = `?server_id=${activeServerId}&channel_id=${currentChannel}&thread_id=${result.thread_id}`;
|
||
} else {
|
||
alert(result.error || 'Failed to create thread');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Forum: Mark as Solution
|
||
document.addEventListener('click', async (e) => {
|
||
const solBtn = e.target.closest('.action-btn.mark-solution');
|
||
if (solBtn) {
|
||
const threadId = solBtn.dataset.threadId;
|
||
const messageId = solBtn.classList.contains('active') ? null : solBtn.dataset.messageId;
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_threads.php?action=solve', {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ thread_id: threadId, message_id: messageId })
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(result.error || 'Failed to update solution');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
});
|
||
|
||
// Forum: Manage Tags
|
||
const manageTagsBtn = document.getElementById('manage-tags-btn');
|
||
const manageTagsModal = document.getElementById('manageTagsModal') ? new bootstrap.Modal(document.getElementById('manageTagsModal')) : null;
|
||
|
||
manageTagsBtn?.addEventListener('click', async () => {
|
||
if (!manageTagsModal) return;
|
||
loadForumAdminTags();
|
||
manageTagsModal.show();
|
||
});
|
||
|
||
async function loadForumAdminTags() {
|
||
const list = document.getElementById('forum-tags-admin-list');
|
||
list.innerHTML = '<div class="text-center p-3 text-muted small">Loading tags...</div>';
|
||
try {
|
||
const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`);
|
||
const data = await resp.json();
|
||
list.innerHTML = '';
|
||
if (data.success && data.tags.length > 0) {
|
||
data.tags.forEach(tag => {
|
||
const div = document.createElement('div');
|
||
div.className = 'd-flex justify-content-between align-items-center mb-2 p-2 bg-dark rounded';
|
||
div.innerHTML = `
|
||
<div class="d-flex align-items-center">
|
||
<div style="width: 12px; height: 12px; border-radius: 50%; background-color: ${tag.color}; margin-right: 8px;"></div>
|
||
<span>${tag.name}</span>
|
||
</div>
|
||
<button class="btn btn-sm text-danger delete-forum-tag-btn" data-id="${tag.id}">×</button>
|
||
`;
|
||
list.appendChild(div);
|
||
});
|
||
} else {
|
||
list.innerHTML = '<div class="text-center p-3 text-muted small">No tags created yet.</div>';
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
document.getElementById('add-forum-tag-btn')?.addEventListener('click', async () => {
|
||
const name = document.getElementById('new-tag-name').value.trim();
|
||
const color = document.getElementById('new-tag-color').value;
|
||
if (!name) return;
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_tags.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'create', channel_id: currentChannel, name, color })
|
||
});
|
||
if ((await resp.json()).success) {
|
||
document.getElementById('new-tag-name').value = '';
|
||
loadForumAdminTags();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
document.getElementById('forum-tags-admin-list')?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-forum-tag-btn')) {
|
||
const tagId = e.target.dataset.id;
|
||
try {
|
||
const resp = await fetch('api_v1_tags.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', channel_id: currentChannel, tag_id: tagId })
|
||
});
|
||
if ((await resp.json()).success) loadForumAdminTags();
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
});
|
||
|
||
// Rules: Add Rule
|
||
const addRuleBtn = document.getElementById('add-rule-btn');
|
||
addRuleBtn?.addEventListener('click', async () => {
|
||
const content = prompt('Rule Content:');
|
||
if (!content) return;
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('channel_id', currentChannel);
|
||
formData.append('content', content);
|
||
const resp = await fetch('api_v1_rules.php', { method: 'POST', body: formData });
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(result.error || 'Failed to add rule');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Rules: Delete/Edit
|
||
document.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-rule-btn')) {
|
||
if (!confirm('Delete this rule?')) return;
|
||
const id = e.target.dataset.id;
|
||
const resp = await fetch(`api_v1_rules.php?id=${id}`, { method: 'DELETE' });
|
||
if ((await resp.json()).success) location.reload();
|
||
}
|
||
if (e.target.classList.contains('edit-rule-btn')) {
|
||
const id = e.target.dataset.id;
|
||
const oldContent = e.target.closest('.rule-item').querySelector('.rule-content').innerText;
|
||
const newContent = prompt('Edit Rule:', oldContent);
|
||
if (!newContent || newContent === oldContent) return;
|
||
|
||
const resp = await fetch('api_v1_rules.php', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id, content: newContent })
|
||
});
|
||
if ((await resp.json()).success) location.reload();
|
||
}
|
||
});
|
||
|
||
// Channel Selection Type
|
||
const addChannelBtns = document.querySelectorAll('.add-channel-btn');
|
||
addChannelBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const type = btn.dataset.type;
|
||
const select = document.getElementById('channel-type-select');
|
||
if (select) {
|
||
select.value = type === 'voice' ? 'voice' : 'chat';
|
||
}
|
||
});
|
||
});
|
||
|
||
// User Settings - Avatar Search
|
||
const avatarSearchBtn = document.getElementById('search-avatar-btn');
|
||
const avatarSearchQuery = document.getElementById('avatar-search-query');
|
||
const avatarResults = document.getElementById('avatar-results');
|
||
const avatarPreview = document.getElementById('settings-avatar-preview');
|
||
const avatarUrlInput = document.getElementById('settings-avatar-url');
|
||
|
||
avatarSearchBtn?.addEventListener('click', async () => {
|
||
const q = avatarSearchQuery.value.trim();
|
||
if (!q) return;
|
||
avatarResults.innerHTML = '<div class="text-muted small">Searching...</div>';
|
||
try {
|
||
const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(q)}`);
|
||
const data = await resp.json();
|
||
avatarResults.innerHTML = '';
|
||
data.forEach(photo => {
|
||
const img = document.createElement('img');
|
||
img.src = photo.url;
|
||
img.className = 'avatar-pick';
|
||
img.style.width = '60px';
|
||
img.style.height = '60px';
|
||
img.style.cursor = 'pointer';
|
||
img.onclick = () => {
|
||
avatarUrlInput.value = photo.url;
|
||
avatarPreview.style.backgroundImage = `url('${photo.url}')`;
|
||
};
|
||
avatarResults.appendChild(img);
|
||
});
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Toggle members sidebar
|
||
const toggleMembersBtn = document.getElementById('toggle-members-btn');
|
||
const membersSidebar = document.querySelector('.members-sidebar');
|
||
if (toggleMembersBtn && membersSidebar) {
|
||
toggleMembersBtn.addEventListener('click', () => {
|
||
if (window.innerWidth > 992) {
|
||
membersSidebar.classList.toggle('hidden');
|
||
} else {
|
||
membersSidebar.classList.toggle('show');
|
||
}
|
||
});
|
||
}
|
||
|
||
// User Settings - Save
|
||
const saveSettingsBtn = document.getElementById('save-settings-btn');
|
||
saveSettingsBtn?.addEventListener('click', async () => {
|
||
const form = document.getElementById('user-settings-form');
|
||
const formData = new FormData(form);
|
||
const dndMode = document.getElementById('dnd-switch').checked ? '1' : '0';
|
||
formData.append('dnd_mode', dndMode);
|
||
|
||
const theme = form.querySelector('input[name="theme"]:checked').value;
|
||
document.body.setAttribute('data-theme', theme);
|
||
|
||
const resp = await fetch('api_v1_user.php', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(result.error || 'Failed to save settings');
|
||
}
|
||
});
|
||
});
|
||
|
||
function escapeHTML(str) {
|
||
const div = document.createElement('div');
|
||
div.textContent = str;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
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;">${escapeHTML(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;">${escapeHTML(meta.title)}</a>` : ''}
|
||
${meta.description ? `<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);">${escapeHTML(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 hasManageRights = window.canManageChannels || window.isServerOwner || false;
|
||
|
||
const pinHtml = `
|
||
<span class="action-btn pin ${msg.is_pinned ? 'active' : ''}" title="${msg.is_pinned ? 'Unpin' : 'Pin'}" data-id="${msg.id}" data-pinned="${msg.is_pinned ? '1' : '0'}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
|
||
</span>
|
||
`;
|
||
|
||
const actionsHtml = (isMe || hasManageRights) ? `
|
||
<div class="message-actions-menu">
|
||
${pinHtml}
|
||
${isMe ? `
|
||
<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>
|
||
` : '';
|
||
|
||
const pinnedBadge = msg.is_pinned ? `
|
||
<span class="pinned-badge ms-2" title="Pinned Message">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path></svg>
|
||
Pinned
|
||
</span>
|
||
` : '';
|
||
|
||
const mentionRegex = new RegExp(`@${window.currentUsername}\\b`, 'g');
|
||
if (msg.content.match(mentionRegex)) {
|
||
div.classList.add('mentioned');
|
||
}
|
||
if (msg.is_pinned) div.classList.add('pinned');
|
||
|
||
const ytRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
|
||
const ytMatch = msg.content.match(ytRegex);
|
||
let ytHtml = '';
|
||
if (ytMatch && ytMatch[1]) {
|
||
ytHtml = `<div class="youtube-embed mt-2"><iframe width="100%" height="315" src="https://www.youtube.com/embed/${ytMatch[1]}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="border-radius: 8px; max-width: 560px;"></iframe></div>`;
|
||
}
|
||
|
||
const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
|
||
|
||
div.innerHTML = `
|
||
<div class="message-avatar" style="${avatarStyle}"></div>
|
||
<div class="message-content">
|
||
<div class="message-author" style="${authorStyle}">
|
||
${escapeHTML(msg.username)}
|
||
<span class="message-time">${msg.time}</span>
|
||
${pinnedBadge}
|
||
${actionsHtml}
|
||
</div>
|
||
<div class="message-text">
|
||
${escapeHTML(msg.content).replace(/\n/g, '<br>').replace(mentionRegex, `<span class="mention">@${window.currentUsername}</span>`)}
|
||
${attachmentHtml}
|
||
${ytHtml}
|
||
${ytHtml ? '' : 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);
|
||
}
|