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');
function scrollToBottom(force = false) {
if (!messagesList) return;
// Smart scroll: only scroll if user is already at the bottom or if forced (e.g. sending a message)
const threshold = 150; // pixels margin
const isAtBottom = messagesList.scrollHeight - messagesList.scrollTop <= messagesList.clientHeight + threshold;
if (force || isAtBottom) {
messagesList.scrollTo({
top: messagesList.scrollHeight,
behavior: 'smooth'
});
// Backup for non-smooth support or rendering delays
setTimeout(() => {
if (force || messagesList.scrollHeight - messagesList.scrollTop <= messagesList.clientHeight + threshold + 200) {
messagesList.scrollTop = messagesList.scrollHeight;
}
}, 100);
}
}
// Emoji list for reactions
const EMOJIS = ['๐', 'โค๏ธ', '๐', '๐ฎ', '๐ข', '๐ฅ', 'โ
', '๐', 'โ', '๐ก', '๐', '๐ฏ'];
// Scroll to bottom
scrollToBottom(true);
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);
// 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, '
');
} 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();
// Polling as fallback for real-time
let lastMessageId = 0;
const findLastMessageId = () => {
const items = document.querySelectorAll('.message-item');
if (items.length > 0) {
lastMessageId = Math.max(...Array.from(items).map(i => parseInt(i.dataset.id) || 0));
}
};
findLastMessageId();
setInterval(async () => {
if (!currentChannel) return;
try {
const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&after_id=${lastMessageId}`);
const data = await resp.json();
if (data.success && data.messages && data.messages.length > 0) {
data.messages.forEach(msg => {
appendMessage(msg);
});
}
} catch (e) { }
}, 1000);
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) {
appendMessage(result.message);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
data: JSON.stringify({
...result.message,
channel_id: currentChannel
})
}));
}
} 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} ${r.count}`;
container.appendChild(badge);
});
container.appendChild(addBtn);
}
function updatePresenceUI(userId, status) {
const memberItem = document.querySelector(`.start-dm-btn[data-user-id="${userId}"] .message-avatar`);
if (memberItem) {
let indicator = memberItem.querySelector('.presence-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.className = 'presence-indicator';
memberItem.appendChild(indicator);
}
indicator.style.position = 'absolute';
indicator.style.bottom = '0';
indicator.style.right = '0';
indicator.style.width = '10px';
indicator.style.height = '10px';
indicator.style.borderRadius = '50%';
indicator.style.border = '2px solid var(--bg-members)';
indicator.style.backgroundColor = status === 'online' ? '#23a559' : '#80848e';
}
}
// Voice
if (voiceHandler) {
document.querySelectorAll('.voice-item').forEach(item => {
item.addEventListener('click', () => {
const cid = item.dataset.channelId;
if (voiceHandler.currentChannelId == cid) {
voiceHandler.leave();
item.classList.remove('active');
} else {
voiceHandler.join(cid);
document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
}
});
});
}
// Message Actions (Edit/Delete)
document.addEventListener('click', async (e) => {
const editBtn = e.target.closest('.action-btn.edit');
if (editBtn) {
const msgId = editBtn.dataset.id;
const msgItem = editBtn.closest('.message-item');
const textEl = msgItem.querySelector('.message-text');
const originalContent = textEl.innerText;
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control bg-dark text-white';
input.value = originalContent;
textEl.innerHTML = '';
textEl.appendChild(input);
input.focus();
input.onkeydown = async (ev) => {
if (ev.key === 'Enter') {
const newContent = input.value.trim();
if (newContent && newContent !== originalContent) {
const resp = await fetch('api_v1_messages.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: msgId, content: newContent })
});
if ((await resp.json()).success) {
textEl.innerHTML = newContent.replace(/\n/g, '
');
ws?.send(JSON.stringify({ type: 'message_edit', message_id: msgId, content: newContent }));
}
} else {
textEl.innerHTML = originalContent.replace(/\n/g, '
');
}
} else if (ev.key === 'Escape') {
textEl.innerHTML = originalContent.replace(/\n/g, '
');
}
};
return;
}
const deleteBtn = e.target.closest('.action-btn.delete');
if (deleteBtn) {
if (!confirm('Delete this message?')) return;
const msgId = deleteBtn.dataset.id;
const resp = await fetch('api_v1_messages.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: msgId })
});
if ((await resp.json()).success) {
deleteBtn.closest('.message-item').remove();
ws?.send(JSON.stringify({ type: 'message_delete', message_id: msgId }));
}
return;
}
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 = '
Loading pinned messages...
';
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 = `
${escapeHTML(msg.username)}
${msg.time}
${escapeHTML(msg.content).replace(/\n/g, '
')}
`;
container.appendChild(div);
});
} else {
container.innerHTML = 'No pinned messages in this channel.
';
}
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 = `
${res.username}
Click to start conversation
`;
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 = `
${res.username}
${res.content}
`;
}
searchResults.appendChild(item);
});
searchResults.style.display = 'block';
} else {
searchResults.innerHTML = 'No results found
';
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 = 'Loading permissions...
';
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 = `
${role.name}
`;
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 = 'No role overrides.
';
return;
}
permissions.forEach(p => {
const item = document.createElement('div');
item.className = 'list-group-item bg-transparent text-white border-secondary p-2';
item.innerHTML = `
`;
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 = 'Loading feeds...
';
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 = 'No RSS feeds configured.
';
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 = `
${feed.url}
Last fetched: ${feed.last_fetched_at || 'Never'}
`;
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 = 'Loading...
';
if (membersList) membersList.innerHTML = 'Loading...
';
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 = 'No roles created yet.
';
}
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 = `
`;
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 += `
`;
});
item.innerHTML = `
${member.username}
${rolesHtml || 'No roles available'}
`;
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 += `
`;
});
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 = 'Loading webhooks...
';
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 = 'No webhooks found.
';
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 = `
${wh.name}
Channel: #${wh.channel_name}
`;
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 = `${user.username}${user.message_count} msgs`;
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 = `
${day.date}
${day.count}
`;
activity.appendChild(bar);
});
if (data.stats.history.length === 0) {
activity.innerHTML = 'No activity in the last 7 days.
';
}
}
} 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 = 'Searching...
';
try {
const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(query)}`);
const data = await resp.json();
serverIconResults.innerHTML = '';
data.forEach(photo => {
const img = document.createElement('img');
img.src = photo.url;
img.className = 'avatar-pick';
img.style.width = '50px';
img.style.height = '50px';
img.onclick = () => {
serverIconUrlInput.value = photo.url;
serverIconPreview.style.backgroundImage = `url('${photo.url}')`;
serverIconResults.innerHTML = '';
};
serverIconResults.appendChild(img);
});
} catch (e) {
serverIconResults.innerHTML = 'Error fetching icons
';
}
});
// 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 = 'Loading tags...
';
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 = 'No tags available.
';
}
} 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 = 'Loading tags...
';
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 = `
`;
list.appendChild(div);
});
} else {
list.innerHTML = 'No tags created yet.
';
}
} 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 = 'Searching...
';
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) {
if (!msg || !msg.id) return;
if (document.querySelector(`.message-item[data-id="${msg.id}"]`)) return;
// Auto-populate metadata for video platforms if missing
const dmRegexForMeta = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/;
const dmMatchForMeta = msg.content.match(dmRegexForMeta);
if (dmMatchForMeta && !msg.metadata) {
msg.metadata = {
title: 'Dailymotion Video',
url: dmMatchForMeta[0],
image: `https://www.dailymotion.com/thumbnail/video/${dmMatchForMeta[1]}`,
site_name: 'Dailymotion'
};
}
const messagesList = document.getElementById('messages-list');
const div = document.createElement('div');
div.className = 'message-item';
div.dataset.id = msg.id;
if (parseInt(msg.id) > lastMessageId) {
lastMessageId = parseInt(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 = ``;
} else {
attachmentHtml = ``;
}
}
let embedHtml = '';
if (msg.metadata) {
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata;
embedHtml = `
${meta.site_name ? `
${escapeHTML(meta.site_name)}
` : ''}
${meta.title ? `
${escapeHTML(meta.title)}` : ''}
${meta.description ? `
${escapeHTML(meta.description)}
` : ''}
${meta.image ? `
` : ''}
`;
}
const isMe = msg.user_id == window.currentUserId || msg.username == window.currentUsername;
const hasManageRights = window.canManageChannels || window.isServerOwner || false;
const pinHtml = `
`;
const actionsHtml = (isMe || hasManageRights) ? `
` : '';
const pinnedBadge = msg.is_pinned ? `
Pinned
` : '';
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=|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
const dmRegex = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/;
const vimeoRegex = /(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/;
const ytMatch = msg.content.match(ytRegex);
const dmMatch = msg.content.match(dmRegex);
const vimeoMatch = msg.content.match(vimeoRegex);
let videoHtml = '';
if (ytMatch && ytMatch[1]) {
videoHtml = ``;
} else if (dmMatch && dmMatch[1]) {
videoHtml = ``;
} else if (vimeoMatch && vimeoMatch[1]) {
videoHtml = ``;
}
const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
div.innerHTML = `
${escapeHTML(msg.content).replace(/\n/g, '
').replace(mentionRegex, `@${window.currentUsername}`)}
${attachmentHtml}
${videoHtml}
${embedHtml}
+
${actionsHtml}
`;
messagesList.appendChild(div);
scrollToBottom(isMe);
// Ensure we scroll again when images/videos load
div.querySelectorAll('img, iframe').forEach(el => {
el.addEventListener('load', () => scrollToBottom(isMe));
});
}
});