38443-vm/assets/js/main.js
Flatlogic Bot 5d6fd46690 v12
2026-02-15 16:11:50 +00:00

1492 lines
68 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

document.addEventListener('DOMContentLoaded', () => {
const fileUpload = document.getElementById('file-upload');
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const messagesList = document.getElementById('messages-list');
const typingIndicator = document.getElementById('typing-indicator');
// Emoji list for reactions
const EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🔥', '✅', '🚀', '❓', '💡', '📌', '💯'];
// Scroll to bottom
messagesList.scrollTop = messagesList.scrollHeight;
const currentChannel = new URLSearchParams(window.location.search).get('channel_id') || 1;
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);
}