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);
}
}
// Unified Emoji Categories - Expanded for "Complete" feel
const EMOJI_CATEGORIES = {
'Smileys': ['๐', '๐', '๐', '๐', '๐', '๐
', '๐คฃ', '๐', '๐', '๐', '๐', '๐', '๐', '๐ฅฐ', '๐', '๐คฉ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐คช', '๐', '๐ค', '๐ค', '๐คญ', '๐คซ', '๐ค', '๐ค', '๐คจ', '๐', '๐', '๐ถ', '๐', '๐', '๐', '๐ฌ', '๐คฅ', '๐', '๐', '๐ช', '๐คค', '๐ด', '๐ท', '๐ค', '๐ค', '๐คข', '๐คฎ', '๐คง', '๐ฅต', '๐ฅถ', '๐ฅด', '๐ต', '๐คฏ', '๐ค ', '๐ฅณ', '๐', '๐ค', '๐ง', '๐', '๐', '๐', 'โน๏ธ', '๐ฎ', '๐ฏ', '๐ฒ', '๐ณ', '๐ฅบ', '๐ฆ', '๐ง', '๐จ', '๐ฐ', '๐ฅ', '๐ข', '๐ญ', '๐ฑ', '๐', '๐ฃ', '๐', '๐', '๐ฉ', '๐ซ', '๐ฅฑ', '๐ค', '๐ก', '๐ ', '๐คฌ', '๐', '๐ฟ', '๐น', '๐บ', '๐', 'โ ๏ธ', '๐ฉ', '๐คก', '๐ป', '๐ฝ', '๐พ', '๐ค', '๐บ', '๐ธ', '๐ป', '๐ผ', '๐ฝ', '๐', '๐ฟ', '๐พ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', 'โฃ๏ธ', '๐', 'โค๏ธ', '๐งก', '๐', '๐', '๐', '๐', '๐ค', '๐ค', '๐ค', '๐ฏ', '๐ข', '๐ฅ', '๐ซ', '๐ฆ', '๐จ', '๐ณ๏ธ', '๐ฃ', '๐ฌ', '๐๏ธโ๐จ๏ธ', '๐จ๏ธ', '๐ฏ๏ธ', '๐ญ', '๐ค', '๐ช', '๐ ', '๐', '๐', '๐', '๐', '๐๏ธ', '๐', '๐'],
'Gestures': ['๐', '๐ค', '๐๏ธ', 'โ', '๐', '๐', '๐ค', 'โ๏ธ', '๐ค', '๐ค', '๐ค', '๐ค', '๐', '๐', '๐', '๐', '๐', 'โ๏ธ', '๐', '๐', 'โ', '๐', '๐ค', '๐ค', '๐', '๐', '๐', '๐คฒ', '๐ค', '๐', 'โ๏ธ', '๐
', '๐คณ', '๐ช', '๐ฆพ', '๐ฆต', '๐ฆฟ', '๐ฆถ', '๐', '๐ฆป', '๐', '๐ง ', '๐ฆท', '๐ฆด', '๐', '๐๏ธ', '๐
', '๐', '๐', '๐ค', '๐ค', '๐๏ธ', '๐', '๐', 'โ๏ธ', '๐คณ', '๐ช', '๐ฆพ'],
'People': ['๐ถ', '๐ง', '๐ฆ', '๐ง', '๐ง', '๐ฑ', '๐จ', '๐ฉ', '๐ง', '๐ด', '๐ต', '๐ฎ', '๐ต๏ธ', '๐', '๐ท', '๐คด', '๐ธ', '๐ณ', '๐ฒ', '๐ง', '๐คต', '๐ฐ', '๐คฐ', '๐คฑ', '๐ผ', '๐
', '๐คถ', '๐ฆธ', '๐ฆน', '๐ง', '๐ง', '๐ง', '๐ง', '๐ง', '๐ง', '๐ง', '๐', '๐', '๐ถ', '๐', '๐', '๐บ', '๐ด๏ธ', '๐ฏ', '๐ง', '๐ง', '๐คบ', '๐', 'โท๏ธ', '๐', '๐๏ธ', '๐', '๐ฃ', '๐', 'โน๏ธ', '๐๏ธ', '๐ด', '๐ต', '๐คธ', '๐คผ', '๐คฝ', '๐คพ', '๐คน', '๐ง', '๐', '๐'],
'Animals': ['๐ถ', '๐ฑ', '๐ญ', '๐น', '๐ฐ', '๐ฆ', '๐ป', '๐ผ', '๐จ', '๐ฏ', '๐ฆ', '๐ฎ', '๐ท', '๐ฝ', '๐ธ', '๐ต', '๐', '๐', '๐', '๐', '๐', '๐ง', '๐ฆ', '๐ค', '๐ฃ', '๐ฅ', '๐ฆ', '๐ฆ
', '๐ฆ', '๐ฆ', '๐บ', '๐', '๐ด', '๐ฆ', '๐', '๐', '๐ฆ', '๐', '๐', '๐', '๐ฆ', '๐ฆ', '๐ท๏ธ', '๐ธ๏ธ', '่ ', '๐ข', '๐', '๐ฆ', '๐ฆ', '๐ฆ', '๐', '๐ฆ', '๐ฆ', '๐ฆ', '๐ฆ', '๐ก', '๐ ', '๐', '๐ฌ', '๐ณ', '๐', '๐ฆ', '๐', '๐
', '๐', '๐ฆ', '๐ฆ', '๐ฆง', '๐', '๐ฆ', '๐ฆ', '๐ช', '๐ซ', '๐ฆ', '๐ฆ', '๐ฆฌ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐ฆ', '๐', '๐ฉ', '๐ฆฎ', '๐โ๐ฆบ', '๐', '๐โโฌ', '๐', '๐ฆ', '๐ฆ', '๐ฆ', '๐ฆข', '๐ฆฉ', '๐๏ธ', '๐', '๐ฆ', '๐ฆจ', '๐ฆก', '๐ฆฆ', '๐ฆฅ', '๐', '๐', '๐ฟ๏ธ', '๐ฆ', '๐พ', '๐', '๐ฒ', '๐ต', '๐', '๐ฒ', '๐ณ', '๐ด', '๐ฑ', '๐ฟ', 'โ๏ธ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐พ'],
'Nature': ['๐', '๐ท', '๐น', '๐ฅ', '๐บ', '๐ธ', '๐ผ', '๐ป', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐ช', '๐ซ', 'โญ๏ธ', '๐', 'โจ', 'โก๏ธ', 'โ๏ธ', '๐ฅ', '๐ฅ', '๐ช๏ธ', '๐', 'โ๏ธ', '๐ค๏ธ', 'โ
๏ธ', '๐ฅ๏ธ', 'โ๏ธ', '๐ฆ๏ธ', '๐ง๏ธ', '๐จ๏ธ', '๐ฉ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', '๐ฌ๏ธ', '๐จ', '๐ง', '๐ฆ', 'โ๏ธ', 'โ๏ธ', '๐', '๐ซ๏ธ', 'โฐ๏ธ', '๐๏ธ', '๐ป', '๐', '๐๏ธ', '๐๏ธ', '๐๏ธ', '๐๏ธ', 'โบ๏ธ'],
'Food': ['๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐ฅญ', '๐', '๐ฅฅ', '๐ฅ', '๐
', '๐', '๐ฅ', '๐ฅฆ', '๐ฅฌ', '๐ฅ', '๐ฝ', '๐ฅ', '๐ง', '๐ง
', '๐', '๐ฅ', '๐ฐ', '๐', '๐ฅ', '๐ฅ', '๐ฅจ', '๐ฅฏ', '๐ฅ', '๐ง', '๐ง', '๐', '๐', '๐ฅฉ', '๐ฅ', '๐', '๐', '๐', '๐ญ', '๐ฅช', '๐ฎ', '๐ฏ', '๐ฅ', '๐ง', '๐ณ', '๐ฅ', '๐ฒ', '๐ฅฃ', '๐ฅ', '๐ฟ', 'ใใฟใผ', '๐ง', '๐ฅซ', '๐ฑ', '๐', '๐', '๐', '๐', '๐', '๐', '๐ ', '๐ข', '๐ฃ', '๐ค', '๐ฅ', '๐ฅฎ', '๐ก', '๐ฅ', '๐ฅ ', '๐ฅก', '๐ฆ', '๐ง', '๐จ', '๐ฉ', '๐ช', '๐', '๐ฐ', '๐ง', '๐ฅง', '๐ซ', '๐ฌ', '๐ญ', '๐ฎ', '๐ฏ', '๐ผ', '๐ฅ', 'โ๏ธ', '๐ต', '๐ง', '๐ฅค', '๐ง', '๐บ', '๐ป', '๐ฅ', '๐ท', '๐ฅ', '๐ธ', '๐น', '๐พ', '๐ง', '๐ฅ', '๐ด', '๐ฝ๏ธ'],
'Activities': ['โฝ๏ธ', '๐', '๐', 'โพ๏ธ', '๐ฅ', '๐พ', '๐', '๐', '๐ฑ', '๐', '๐ธ', '๐ฅ
', '๐', '๐', '๐', 'โณ๏ธ', '๐น', '๐ฃ', '๐ฅ', '๐ฅ', '๐น', '๐ท', 'โธ๏ธ', '๐ฅ', '๐ฟ', 'โท๏ธ', '๐', '๐๏ธ', '๐คบ', '๐คผ', '๐คธ', 'โน๏ธ', '๐คฝ', '๐คพ', '๐คน', '๐ง', '๐', '๐ฃ', '๐', '๐ด', '๐ต', '๐ง', '๐๏ธ', '๐', '๐
', '๐ฅ', '๐ฅ', '๐ฅ', '๐ซ', '๐๏ธ', '๐ญ', '๐จ', '๐ฌ', '๐ค', '๐ง', '๐ผ', '๐น', '๐ฅ', '๐ท', '๐บ', '๐ธ', '๐ช', '๐ป', '๐ฒ', 'โ๏ธ', '๐ฏ', 'ใณใ', '๐ฎ', '๐ฐ', '๐งฉ'],
'Travel': ['๐', '๐', '๐', '๐', '๐', '๐๏ธ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐ต', '๐ฒ', '๐ด', '๐', '๐ฃ๏ธ', '๐ค๏ธ', 'โฝ๏ธ', '๐จ', '๐ฅ', '๐ฆ', '๐ง', 'โ๏ธ', 'โต๏ธ', '๐ค', '๐ณ๏ธ', 'โด๏ธ', '๐ข', 'โ๏ธ', '๐ซ', '๐ฌ', '๐บ', '๐', '๐', 'ใฑใผใใซ', '๐ก', '๐', '๐ธ', '๐ฐ๏ธ', 'โ๏ธ', 'โณ', 'โ๏ธ', 'โฐ', 'โฑ๏ธ', 'โฒ๏ธ', '๐ฐ๏ธ', '๐ก๏ธ', '๐', '๐๏ธ', '๐', '๐
', '๐', '๐', '๐', '๐ ', '๐ก', '๐ข', '๐', '๐', '๐', '๐
', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐'],
'Objects': ['โ๏ธ', '๐ฑ', '๐ฒ', '๐ป', 'โจ๏ธ', '๐ฑ๏ธ', '๐ฒ๏ธ', '๐น๏ธ', '๐๏ธ', '๐ฝ', '๐พ', '๐ฟ', '๐', '๐ผ', '๐ท', '๐ธ', '๐น', '๐ฅ', '๐ฝ๏ธ', '๐๏ธ', '๐', '๐ ', '๐บ', '๐ป', '๐๏ธ', '๐๏ธ', '๐๏ธ', '๐งญ', 'โฑ๏ธ', 'โฒ๏ธ', 'โฐ', '๐ฐ๏ธ', 'โ๏ธ', 'โณ', '๐ก', '๐', 'ใใฉใฐ', '๐ก', '๐ฆ', '๐ฏ๏ธ', '๐ช', '๐งฏ', '๐ข๏ธ', '๐ธ', '๐ต', '๐ด', '๐ถ', '๐ท', '๐ฐ', '๐ณ', '๐', 'โ๏ธ', '๐งฐ', 'ใฌใณใ', '๐จ', 'โ๏ธ', '๐ ๏ธ', 'โ๏ธ', 'ใใใ', 'โ๏ธ', '๐งฑ', '้', '๐งฒ', '๐ซ', '๐ฃ', '๐งจ', '๐ช', 'ใใคใ', '๐ก๏ธ', 'โ๏ธ', '็พ', '๐ฌ', 'โฐ๏ธ', 'โฑ๏ธ', '๐บ', 'ๆฐดๆถ', '๐งฟ', '๐ฟ', '๐', 'โ๏ธ', 'ๆ้ ้ก', '๐ฌ', '๐ณ๏ธ', '๐', '๐', '๐ฉธ', 'DNA', '๐ฆ ', '๐งซ', '๐งช', '๐ก๏ธ', '๐งน', 'ใซใด', '๐งป', '็ณ้นธ', 'ในใใณใธ', '๐ช', 'ใญใผใทใงใณ', '๐๏ธ', '้ต', '๐๏ธ', 'ใใข', 'ๆค
ๅญ', 'ใฝใใก', 'ใใใ', '๐', 'ใใใฃใใข', '้ก็ธ', '่ข', 'ใซใผใ', '๐', '๐', '๐', 'ใชใใณ', '๐', '๐', 'ไบบๅฝข', 'ๆ็ฏ', '๐', '๐งง', 'โ๏ธ', '๐ฉ', '๐จ', '๐ง', '๐', '๐ฅ', '๐ค', '๐ฆ', '๐ท๏ธ', 'ใใฉใซใ', '๐', 'ใซใฌใณใใผ', '๐', '๐๏ธ', '๐๏ธ', '๐', 'ใใฃใผใ', '๐', '๐', 'ใฏใชใใใใผใ', '็ป้ฒ', '๐', '๐', '๐๏ธ', 'ๅฎ่ฆ', '๐', 'ใใตใ', '๐๏ธ', 'ใญใฃใใใใ', 'ใดใ็ฎฑ', '๐', '๐', '๐', '๐', '้ต', '๐๏ธ'],
'Symbols': ['๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', 'โฃ๏ธ', '๐', 'โค๏ธ', '๐งก', '๐', '๐', '๐', '๐', '๐ค', '๐ค', '๐ค', '๐ฏ', '๐ข', '๐ฅ', '๐ซ', '๐ฆ', '๐จ', '๐ณ๏ธ', '๐ฃ', '๐ฌ', '๐๏ธโ๐จ๏ธ', '๐จ๏ธ', '๐ฏ๏ธ', '๐ญ', '๐ค', '๐', 'โ ๏ธ', 'โฅ๏ธ', 'โฆ๏ธ', 'โฃ๏ธ', 'ใธใงใผใซใผ', '๐๏ธ', '๐ด', '๐', '๐', '๐', '๐', '๐ข', '๐ฃ', '๐ฏ', '๐', '๐', '๐ผ', '๐ต', '๐ถ', '๐น', 'ATM', '๐ฎ', '๐ฐ', 'โฟ๏ธ', '๐น', '๐บ', '๐ป', '๐ผ', '๐พ', '๐', 'ใซในใฟใ ', 'ใใฒใผใธ', '๐
', 'โ ๏ธ', '๐ธ', 'โ๏ธ', '๐ซ', '๐ณ', '๐ฏ', '๐ฑ', '๐ท', '๐ต', '๐', 'ๆพๅฐ่ฝ', 'ใใคใช', 'โฌ๏ธ', 'โ๏ธ', 'โก๏ธ', 'โ๏ธ', 'โฌ๏ธ', 'โ๏ธ', 'โฌ
๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โฉ๏ธ', 'โช๏ธ', 'โคด๏ธ', 'โคต๏ธ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', 'โ๏ธ', '๐๏ธ', 'โก๏ธ', 'โธ๏ธ', 'โฏ๏ธ', 'โ๏ธ', 'โฆ๏ธ', 'โช๏ธ', 'โฎ๏ธ', '๐', '๐ฏ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ', '๐', '๐', '๐', 'โถ๏ธ', 'โฉ', 'โญ๏ธ', 'โฏ๏ธ', 'โ๏ธ', 'โช', 'โฎ๏ธ', '๐ผ', 'โซ', '๐ฝ', 'โฌ', 'โธ๏ธ', 'โน๏ธ', 'โบ๏ธ', 'โ๏ธ', '๐ฆ', '๐
', '๐', '๐ถ', '๐ณ', '๐ด', 'โ', 'โ', 'โ', 'โ๏ธ', 'โพ๏ธ', 'โผ๏ธ', 'โ๏ธ', 'โ', 'โ', 'โ', 'โ๏ธ', 'ใฐ๏ธ', '๐ฑ', '๐ฒ', 'โ๏ธ', 'โป๏ธ', 'โ๏ธ', '๐ฑ', '๐', '๐ฐ', 'โญ๏ธ', 'โ
', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ', 'โ', 'โฐ', 'โฟ', 'ใฝ๏ธ', 'โณ๏ธ', 'โด๏ธ', 'โ๏ธ', 'โผ๏ธ', '๐', '๐๏ธ', '๐ท๏ธ', '๐ถ', '๐ฏ๏ธ', '๐', '๐น', '๐๏ธ', '๐ฒ', '๐', '๐ธ', '๐ด', '๐ณ', 'ใ๏ธ', 'ใ๏ธ', '๐บ', '๐ต', '๐ด', '๐ ', '๐ก', '๐ข', '๐ต', '๐ฃ', '๐ค', 'โซ๏ธ', 'โช๏ธ', '๐ฅ', '๐ง', '๐จ', '๐ฉ', '๐ฆ', '๐ช', '๐ซ', 'โฌ๏ธ', 'โฌ๏ธ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โธ', 'โฆ', 'โฆ', 'โช', 'โฎ', 'โฏ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โพ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ'],
'Flags': ['๐', '๐ฉ', '๐', '๐ด', '๐ณ๏ธ', '๐ณ๏ธโ๐', '๐ณ๏ธโโง๏ธ', '๐ดโโ ๏ธ', '๐ฆ๐ซ', '๐ฆ๐ฝ', '๐ฆ๐ฑ', '๐ฉ๐ฟ', '๐ฆ๐ฒ', '๐ฆ๐บ', '๐ฆ๐น', '๐ฆ๐ฟ', '๐ง๐ช', '๐ง๐ท', '๐จ๐ฆ', '๐จ๐ฑ', '๐จ๐ณ', '๐จ๐ด', '๐จ๐ฟ', '๐ฉ๐ฐ', '๐ช๐ฌ', '๐ซ๐ฎ', '๐ซ๐ท', '๐ฉ๐ช', '๐ฌ๐ท', '๐ญ๐ฐ', '๐ฎ๐ณ', '๐ฎ๐ฉ', '๐ฎ๐ช', '๐ฎ๐ฑ', '๐ฎ๐น', '๐ฏ๐ต', '๐ฐ๐ท', '๐ฒ๐ฝ', '๐ณ๐ฑ', '๐ณ๐ฟ', '๐ณ๐ด', '๐ต๐ฐ', '๐ต๐ญ', '๐ต๐ฑ', '๐ต๐น', '๐ท๐บ', '๐ธ๐ฆ', '๐ธ๐ฌ', '๐ฟ๐ฆ', '๐ช๐ธ', '๐ธ๐ช', '๐จ๐ญ', '๐น๐ญ', '๐น๐ท', '๐บ๐ฆ', '๐ฆ๐ช', '๐ฌ๐ง', '๐บ๐ธ', '๐ป๐ณ', '๐ฆ๐ท', '๐ง๐ฉ', '๐ง๐ช', '๐ง๐ด', '๐ฎ๐ฉ', '๐ฎ๐ท', '๐ฎ๐ถ', '๐ฏ๐ฒ', '๐ฐ๐ฟ', '๐ฐ๐ช', '๐ฒ๐พ', '๐ฒ๐ฆ', '๐ณ๐ฌ', '๐ต๐ช', '๐ท๐ด', '๐ท๐ธ', '๐ธ๐ฐ', '๐บ๐พ', '๐ฟ๐ผ']
};
const categoryIcons = {
'Custom': 'โญ',
'Smileys': '๐',
'Gestures': '๐',
'People': '๐ถ',
'Animals': '๐ถ',
'Nature': '๐ต',
'Food': '๐',
'Activities': 'โฝ๏ธ',
'Travel': '๐',
'Objects': 'โ๏ธ',
'Symbols': 'โค๏ธ',
'Flags': '๐'
};
const ALL_EMOJIS = Object.values(EMOJI_CATEGORIES).flat();
// Unified custom emote loading and caching
window.CUSTOM_EMOTES_CACHE = [];
window.loadCustomEmotes = async () => {
try {
const resp = await fetch('api/emotes.php?action=list');
const data = await resp.json();
if (data.success) {
window.CUSTOM_EMOTES_CACHE = data.emotes || [];
return window.CUSTOM_EMOTES_CACHE;
}
return [];
} catch (e) {
console.error("Failed to load custom emotes", e);
return [];
}
};
// Settings Emotes Tab Logic
async function setupSettingsEmotes() {
console.log("Setting up Emotes Tab...");
const sidebar = document.getElementById('settings-emotes-sidebar');
const grid = document.getElementById('settings-emotes-grid');
const searchInput = document.getElementById('settings-emotes-search');
const uploadZone = document.getElementById('custom-emote-upload-zone');
const uploadInput = document.getElementById('emote-upload-input');
if (!sidebar || !grid) return;
const categories = ['Custom', ...Object.keys(EMOJI_CATEGORIES)];
const renderGrid = async (category, searchTerm = '') => {
grid.innerHTML = '
';
if (category === 'Custom' && !searchTerm) {
if (uploadZone) uploadZone.classList.remove('d-none');
const emotes = await window.loadCustomEmotes();
grid.innerHTML = '';
if (emotes.length === 0) {
grid.innerHTML = 'Aucune emote personnalisรฉe. Ajoutez-en une !
';
} else {
emotes.forEach(emote => {
const div = document.createElement('div');
div.className = 'role-emoji-item rounded d-flex flex-column align-items-center justify-content-center p-2 text-center position-relative';
div.style.cursor = 'pointer';
div.style.backgroundColor = 'var(--separator-soft)';
div.style.height = 'auto';
div.innerHTML = `
${emote.code}
`;
div.onmouseenter = () => div.querySelector('.emote-actions')?.classList.remove('d-none');
div.onmouseleave = () => div.querySelector('.emote-actions')?.classList.add('d-none');
div.onclick = (e) => {
if (e.target.closest('.emote-actions')) return;
navigator.clipboard.writeText(emote.code);
};
div.querySelector('.delete-emote').onclick = async (e) => {
e.stopPropagation();
if (!confirm(`Supprimer l'emote ${emote.code} ?`)) return;
const fd = new FormData();
fd.append('id', emote.id);
const res = await (await fetch('api/emotes.php?action=delete', { method: 'POST', body: fd })).json();
if (res.success) renderGrid('Custom');
};
div.querySelector('.edit-emote').onclick = async (e) => {
e.stopPropagation();
const newName = prompt("Nouveau nom (sans les :)", emote.name);
if (!newName || newName === emote.name) return;
const fd = new FormData();
fd.append('id', emote.id);
fd.append('name', newName);
const res = await (await fetch('api/emotes.php?action=rename', { method: 'POST', body: fd })).json();
if (res.success) renderGrid('Custom');
};
grid.appendChild(div);
});
}
} else {
if (uploadZone) uploadZone.classList.add('d-none');
grid.innerHTML = '';
const list = searchTerm ? ALL_EMOJIS.filter(e => e.includes(searchTerm)) : EMOJI_CATEGORIES[category];
(list || []).forEach(emoji => {
const div = document.createElement('div');
div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-2';
div.style.cursor = 'pointer';
div.style.fontSize = '24px';
div.style.backgroundColor = 'var(--separator-soft)';
div.textContent = emoji;
div.onclick = () => {
navigator.clipboard.writeText(emoji);
};
grid.appendChild(div);
});
}
};
sidebar.innerHTML = '';
categories.forEach((cat, idx) => {
const btn = document.createElement('button');
btn.className = `btn w-100 text-start text-white border-0 py-2 px-3 mb-1 d-flex align-items-center gap-2 ${idx === 0 ? 'active' : ''}`;
btn.style.backgroundColor = idx === 0 ? 'var(--separator)' : 'transparent';
btn.innerHTML = ` ${cat}`;
btn.onclick = async () => {
sidebar.querySelectorAll('button').forEach(b => {
b.classList.remove('active');
b.style.backgroundColor = 'transparent';
});
btn.classList.add('active');
btn.style.backgroundColor = 'var(--separator)';
await renderGrid(cat);
};
sidebar.appendChild(btn);
});
searchInput.oninput = async () => {
const term = searchInput.value.trim();
if (term) {
sidebar.querySelectorAll('button').forEach(b => {
b.classList.remove('active');
b.style.backgroundColor = 'transparent';
});
await renderGrid(null, term);
} else {
const activeBtn = sidebar.querySelector('button.active');
const activeCat = activeBtn ? activeBtn.innerText.trim() : 'Custom';
await renderGrid(activeCat);
}
};
if (uploadInput) {
uploadInput.onchange = async () => {
const file = uploadInput.files[0];
if (!file) return;
const fd = new FormData();
fd.append('emote', file);
const res = await (await fetch('api/emotes.php?action=upload', { method: 'POST', body: fd })).json();
if (res.success) renderGrid('Custom');
else alert(res.error || "Upload failed");
};
}
await renderGrid('Custom');
}
const UniversalEmojiPicker = {
currentPicker: null,
show: async function(anchor, callback, options = {}) {
this.hide();
const picker = document.createElement('div');
picker.className = 'emoji-picker-container rounded shadow-lg p-0 d-flex flex-column';
picker.style.position = 'fixed';
picker.style.zIndex = '10000';
picker.style.width = options.width || '400px';
picker.style.height = options.height || '450px';
picker.style.backgroundColor = '#2b2d31';
picker.style.border = '1px solid #1e1f22';
picker.style.display = 'flex';
picker.style.flexDirection = 'column';
const mainLayout = document.createElement('div');
mainLayout.className = 'd-flex flex-grow-1 overflow-hidden';
const tabs = document.createElement('div');
tabs.className = 'emoji-sidebar d-flex flex-column p-2 border-end border-secondary custom-scrollbar';
tabs.style.width = '60px';
tabs.style.overflowY = 'auto';
tabs.style.backgroundColor = '#1e1f22';
const contentArea = document.createElement('div');
contentArea.className = 'd-flex flex-column flex-grow-1';
const searchContainer = document.createElement('div');
searchContainer.className = 'p-2 border-bottom border-secondary';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search emojis...';
searchInput.className = 'form-control form-control-sm bg-dark border-secondary text-white';
searchContainer.appendChild(searchInput);
const grid = document.createElement('div');
grid.className = 'emoji-grid flex-grow-1 p-2 overflow-auto custom-scrollbar';
grid.style.display = 'grid';
grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(36px, 1fr))';
grid.style.gap = '4px';
grid.style.alignContent = 'start';
const categories = ['Custom', ...Object.keys(EMOJI_CATEGORIES)];
const renderGrid = async (category, searchTerm = '') => {
grid.innerHTML = '';
if (category === 'Custom' && !searchTerm) {
const emotes = await window.loadCustomEmotes();
emotes.forEach(emote => {
const div = document.createElement('div');
div.className = 'emoji-item rounded p-1 text-center';
div.style.cursor = 'pointer';
div.innerHTML = ` `;
div.onclick = () => {
callback(emote.code);
if (!options.keepOpen) this.hide();
};
grid.appendChild(div);
});
} else {
const list = searchTerm ? ALL_EMOJIS.filter(e => e.includes(searchTerm)) : EMOJI_CATEGORIES[category];
(list || []).forEach(emoji => {
const div = document.createElement('div');
div.className = 'emoji-item rounded p-1 text-center';
div.style.cursor = 'pointer';
div.style.fontSize = '20px';
div.textContent = emoji;
div.onclick = () => {
callback(emoji);
if (!options.keepOpen) this.hide();
};
grid.appendChild(div);
});
}
};
categories.forEach(cat => {
const btn = document.createElement('button');
btn.className = 'btn btn-link text-white text-decoration-none p-2 mb-1 opacity-75';
btn.title = cat;
btn.innerHTML = categoryIcons[cat] || '๐';
btn.onclick = async () => {
tabs.querySelectorAll('button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
await renderGrid(cat);
};
tabs.appendChild(btn);
});
searchInput.oninput = async () => {
const term = searchInput.value.trim();
if (term) {
tabs.querySelectorAll('button').forEach(b => b.classList.remove('active'));
await renderGrid(null, term);
} else {
const activeBtn = tabs.querySelector('button.active');
await renderGrid(activeBtn ? activeBtn.title : 'Custom');
}
};
mainLayout.appendChild(tabs);
contentArea.appendChild(searchContainer);
contentArea.appendChild(grid);
mainLayout.appendChild(contentArea);
picker.appendChild(mainLayout);
document.body.appendChild(picker);
this.currentPicker = picker;
// Positioning
const rect = anchor.getBoundingClientRect();
let top = rect.top - picker.offsetHeight - 10;
if (top < 0) top = rect.bottom + 10;
let left = rect.left;
if (left + picker.offsetWidth > window.innerWidth) left = window.innerWidth - picker.offsetWidth - 20;
if (left < 10) left = 10;
picker.style.top = `${top}px`;
picker.style.left = `${left}px`;
await renderGrid('Custom');
const outsideClick = (e) => {
if (!picker.contains(e.target) && e.target !== anchor && !anchor.contains(e.target)) {
this.hide();
document.removeEventListener('click', outsideClick);
}
};
setTimeout(() => document.addEventListener('click', outsideClick), 10);
},
hide() {
if (this.currentPicker) {
this.currentPicker.remove();
this.currentPicker = null;
}
}
};
// Replace old showEmojiPicker and role grid logic
window.showEmojiPicker = (anchor, callback) => UniversalEmojiPicker.show(anchor, callback, { width: '900px', height: '500px' });
window.renderEmojiToElement = (code, el) => {
if (!el) return;
if (!code) {
el.innerHTML = "";
return;
}
if (typeof code === "string" && code.startsWith(':') && code.endsWith(':')) {
const ce = window.CUSTOM_EMOTES_CACHE.find(e => e.code === code);
if (ce) {
el.innerHTML = ` `;
return;
}
}
el.textContent = code;
};
// Unified Emoji Picker & Modal Logic
document.addEventListener("click", (e) => {
// Emoji Picker Triggers
const triggers = {
"role-emoji-select-btn": { target: "edit-role-icon", preview: "selected-role-emoji-preview" },
"add-autorole-emoji-btn": { target: "add-autorole-icon", preview: "add-autorole-emoji-preview" },
"edit-autorole-emoji-btn": { target: "edit-autorole-icon", preview: "edit-autorole-emoji-preview" }
};
const btn = e.target.closest("button[id]");
if (btn && triggers[btn.id]) {
e.preventDefault();
const config = triggers[btn.id];
UniversalEmojiPicker.show(btn, (emoji) => {
const input = document.getElementById(config.target);
const preview = document.getElementById(config.preview);
if (input) input.value = emoji;
window.renderEmojiToElement(emoji, preview);
}, { width: "900px", height: "500px" });
return;
}
// Chat Emoji Picker
const chatEmojiBtn = e.target.closest("#chat-emoji-btn");
if (chatEmojiBtn) {
e.preventDefault();
UniversalEmojiPicker.show(chatEmojiBtn, (emoji) => {
const chatInput = document.getElementById("chat-input");
if (chatInput) {
chatInput.value += emoji;
chatInput.focus();
chatInput.dispatchEvent(new Event('input'));
}
}, { keepOpen: true, width: "900px", height: "500px" });
return;
}
// Autorole Edit modal filling
const editAutoroleBtn = e.target.closest(".edit-autorole-btn");
if (editAutoroleBtn) {
const id = editAutoroleBtn.dataset.id;
const icon = editAutoroleBtn.dataset.icon;
const title = editAutoroleBtn.dataset.title;
const roleId = editAutoroleBtn.dataset.roleId;
const idInput = document.getElementById("edit-autorole-id");
const iconInput = document.getElementById("edit-autorole-icon");
const titleInput = document.getElementById("edit-autorole-title");
const roleIdInput = document.getElementById("edit-autorole-role-id");
const preview = document.getElementById("edit-autorole-emoji-preview");
if (idInput) idInput.value = id;
if (iconInput) iconInput.value = icon;
if (titleInput) titleInput.value = title;
if (roleIdInput) roleIdInput.value = roleId;
if (preview) window.renderEmojiToElement(icon, preview);
return;
}
});
window.loadCustomEmotes();
const emotesTabBtn = document.getElementById('emotes-tab-btn');
if (emotesTabBtn) {
emotesTabBtn.addEventListener('click', () => {
setupSettingsEmotes();
});
}
// Scroll to bottom
scrollToBottom(true);
const currentChannel = window.activeChannelId || 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;
if (typeof VoiceChannel !== 'undefined') {
voiceHandler = new VoiceChannel(null, window.voiceSettings);
window.voiceHandler = voiceHandler;
console.log('VoiceHandler initialized');
// Start global voice sessions polling
setInterval(() => {
VoiceChannel.refreshAllVoiceUsers();
}, 3000);
VoiceChannel.refreshAllVoiceUsers();
}
function connectWS() {
console.log('Connecting to WebSocket...');
try {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Use reverse proxy path /ws
ws = new WebSocket(protocol + '//' + window.location.hostname + '/ws');
ws.onopen = () => {
console.log('WebSocket connected');
if (voiceHandler) voiceHandler.ws = ws;
ws.send(JSON.stringify({
type: 'presence',
user_id: window.currentUserId,
status: 'online'
}));
};
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) {
// For forums, only append if we are in the correct thread
if (window.activeChannelType === 'forum') {
if (!currentThread || data.thread_id != currentThread) {
return;
}
} else if (data.thread_id) {
// If it's not a forum channel but has a thread_id (shouldn't happen with current logic but for safety)
return;
}
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 item = document.querySelector(`.message-item[data-id="${msg.message_id}"]`);
if (item) {
item.dataset.rawContent = msg.content;
const el = item.querySelector('.message-text');
if (el) el.innerHTML = parseCustomEmotes(msg.content);
}
} 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.onclose = () => {
console.log('WebSocket connection closed. Reconnecting...');
setTimeout(connectWS, 3000);
};
} catch (e) {
console.warn('WebSocket connection failed:', e);
}
}
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;
// For forums, if we're not in a thread, don't poll for messages
if (window.activeChannelType === 'forum' && !currentThread) return;
// If we are in a non-forum channel, we should NOT have a currentThread
if (window.activeChannelType !== 'forum' && currentThread) return;
try {
const threadParam = currentThread ? `&thread_id=${currentThread}` : '';
const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&after_id=${lastMessageId}${threadParam}`);
const data = await resp.json();
if (data.success && data.messages && data.messages.length > 0) {
data.messages.forEach(msg => {
// Double check thread_id in JS side too
if (window.activeChannelType === 'forum') {
if (msg.thread_id != currentThread) return;
} else {
if (msg.thread_id) return;
}
appendMessage(msg);
});
}
} catch (e) { }
}, 1000);
function showTyping(username) {
if (!typingIndicator) return;
typingIndicator.textContent = `${username} is typing...`;
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
if (typingIndicator) typingIndicator.textContent = '';
}, 3000);
}
chatInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
chatForm?.dispatchEvent(new Event('submit', { cancelable: true }));
}
});
chatInput?.addEventListener('input', () => {
chatInput.style.height = 'auto';
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
if (chatInput.scrollHeight > 200) {
chatInput.style.overflowY = 'auto';
} else {
chatInput.style.overflowY = 'hidden';
}
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 = '';
chatInput.style.height = '24px';
chatInput.style.overflowY = 'hidden';
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 Click Events
document.addEventListener('click', (e) => {
console.log('Global click at:', e.target);
// Voice Channel Click
const voiceItem = e.target.closest('.voice-item');
if (voiceItem) {
e.preventDefault();
console.log('Voice item clicked, Channel ID:', voiceItem.dataset.channelId);
const channelId = voiceItem.dataset.channelId;
if (voiceHandler) {
if (voiceHandler.currentChannelId == channelId) {
console.log('Already in this channel:', channelId);
return;
} else {
console.log('Joining voice channel:', channelId);
voiceHandler.join(channelId);
// Update active state in UI
document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active'));
voiceItem.classList.add('active');
}
} else {
console.error('voiceHandler not initialized');
}
return;
}
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, (emoji) => toggleReaction(msgId, emoji));
return;
}
});
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 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';
}
}
// Presence indicators initialization (can be expanded)
if (window.currentUserId) {
// ... (existing presence logic if any)
}
// 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 = msgItem.dataset.rawContent || textEl.innerText;
const input = document.createElement('textarea');
input.className = 'form-control bg-dark text-white';
input.style.resize = 'none';
input.style.overflowY = 'hidden';
input.rows = 1;
input.value = originalContent;
textEl.innerHTML = '';
textEl.appendChild(input);
const resizeInput = () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 200) + 'px';
input.style.overflowY = input.scrollHeight > 200 ? 'auto' : 'hidden';
};
input.addEventListener('input', resizeInput);
setTimeout(resizeInput, 0);
input.focus();
input.setSelectionRange(input.value.length, input.value.length);
input.onkeydown = async (ev) => {
if (ev.key === 'Enter' && !ev.shiftKey) {
ev.preventDefault();
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 = parseCustomEmotes(newContent);
msgItem.dataset.rawContent = newContent;
ws?.send(JSON.stringify({ type: 'message_edit', message_id: msgId, content: newContent }));
}
} else {
textEl.innerHTML = parseCustomEmotes(originalContent);
}
} else if (ev.key === 'Escape') {
textEl.innerHTML = parseCustomEmotes(originalContent);
}
};
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.dataset.id = msg.id;
div.dataset.rawContent = msg.content;
div.style.backgroundColor = 'transparent';
const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
div.innerHTML = `
${escapeHTML(msg.username)}
${renderRoleIconJS(msg.role_icon, '14px')}
${msg.time}
${parseCustomEmotes(msg.content)}
`;
container.appendChild(div);
});
} else {
container.innerHTML = 'No pinned messages in this channel.
';
}
return;
}
// Member Menu
const memberItem = e.target.closest('.member-item');
if (memberItem) {
const userId = memberItem.dataset.userId;
const username = memberItem.dataset.username;
const avatar = memberItem.dataset.avatar;
const roleIds = (memberItem.dataset.roleIds || '').split(',').filter(id => id);
// Create or show member menu
document.querySelector('.member-context-menu')?.remove();
const menu = document.createElement('div');
menu.className = 'member-context-menu bg-dark border border-secondary rounded p-2';
menu.style.position = 'fixed';
menu.style.zIndex = '1000';
menu.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)';
menu.style.minWidth = '180px';
const rect = memberItem.getBoundingClientRect();
menu.style.top = `${rect.top}px`;
menu.style.left = `${rect.left - 190}px`;
let rolesHtml = '';
if (roleIds.length > 0) {
// Deduplicate and filter valid roles from serverRoles
const uniqueRoleIds = [...new Set(roleIds)];
const roles = uniqueRoleIds.map(id => serverRoles.find(r => r.id == id)).filter(r => r);
if (roles.length > 0) {
rolesHtml = `
Rรดles
${roles.map(r => `
${r.icon_url ? ` ` : ''}
${escapeHTML(r.name)}
`).join('')}
`;
}
}
menu.innerHTML = `
${rolesHtml}
${(window.isServerOwner || window.canManageServer) ? `` : ''}
`;
document.body.appendChild(menu);
// Close menu on click outside
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
menu.remove();
document.removeEventListener('mousedown', closeMenu);
}
};
document.addEventListener('mousedown', closeMenu);
menu.querySelectorAll('.member-menu-action').forEach(btn => {
btn.onclick = async () => {
const action = btn.dataset.action;
if (action === 'message') {
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}`;
}
} else if (action === 'edit-roles') {
openEditUserRolesModal(userId, username, avatar);
}
menu.remove();
};
});
}
});
// 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 = `
${escapeHTML(res.username)}
${parseCustomEmotes(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 channelPermissionsRolesList = document.getElementById('channel-permissions-roles-list');
const addPermRoleList = document.getElementById('add-permission-role-list');
const channelPermissionsSettings = document.getElementById('channel-permissions-settings');
const noRoleSelectedView = document.getElementById('no-role-selected-view');
const selectedPermRoleName = document.getElementById('selected-perm-role-name');
const removeSelectedPermRole = document.getElementById('remove-selected-perm-role');
const permissionsTogglesContainer = document.getElementById('permissions-toggles-container');
let currentSelectedOverrideRole = null;
let channelPermissionsData = [];
channelPermissionsTabBtn?.addEventListener('click', async () => {
const channelId = document.getElementById('edit-channel-id').value;
currentSelectedOverrideRole = null;
channelPermissionsSettings.classList.add('d-none');
noRoleSelectedView.classList.remove('d-none');
await loadChannelPermissions(channelId);
await loadRolesForPermissions(channelId);
});
const searchChannelPerms = document.getElementById('search-channel-perms');
searchChannelPerms?.addEventListener('input', () => {
const query = searchChannelPerms.value.toLowerCase();
const items = channelPermissionsRolesList.querySelectorAll('.list-group-item');
items.forEach(item => {
const name = item.textContent.toLowerCase();
if (name.includes(query)) {
item.classList.remove('d-none');
} else {
item.classList.add('d-none');
}
});
});
async function loadChannelPermissions(channelId) {
channelPermissionsRolesList.innerHTML = 'Loading...
';
const resp = await fetch(`api_v1_channel_permissions.php?channel_id=${channelId}`);
const data = await resp.json();
if (data.success) {
channelPermissionsData = data.permissions;
renderRoleOverridesList(channelId);
}
}
async function loadRolesForPermissions(channelId) {
if (!addPermRoleList) return;
addPermRoleList.innerHTML = 'Loading... ';
try {
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
const data = await resp.json();
if (data.success) {
addPermRoleList.innerHTML = '';
// Roles Section
const existingRoleIds = channelPermissionsData.filter(p => p.type === 'role').map(p => parseInt(p.role_id));
const availableRoles = data.roles.filter(role => !existingRoleIds.includes(parseInt(role.id)));
if (availableRoles.length > 0) {
const header = document.createElement('li');
header.innerHTML = '';
addPermRoleList.appendChild(header);
availableRoles.forEach(role => {
const li = document.createElement('li');
li.innerHTML = `
${role.name}
`;
li.onclick = async (e) => {
e.preventDefault();
const postResp = 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 })
});
const postData = await postResp.json();
if (postData.success) {
await loadChannelPermissions(channelId);
await loadRolesForPermissions(channelId);
selectOverrideItem(role.id, role.name, 'role');
}
};
addPermRoleList.appendChild(li);
});
}
// Members Section
const existingUserIds = channelPermissionsData.filter(p => p.type === 'member').map(p => parseInt(p.user_id));
const availableMembers = data.members.filter(m => !existingUserIds.includes(parseInt(m.id)));
if (availableMembers.length > 0) {
const header = document.createElement('li');
header.innerHTML = '';
addPermRoleList.appendChild(header);
availableMembers.forEach(m => {
const li = document.createElement('li');
li.innerHTML = `
${m.username}
`;
li.onclick = async (e) => {
e.preventDefault();
const postResp = await fetch('api_v1_channel_permissions.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel_id: channelId, user_id: m.id, allow: 0, deny: 0 })
});
const postData = await postResp.json();
if (postData.success) {
await loadChannelPermissions(channelId);
await loadRolesForPermissions(channelId);
selectOverrideItem(m.id, m.username, 'member');
}
};
addPermRoleList.appendChild(li);
});
}
}
} catch (err) {
console.error(err);
}
}
function renderRoleOverridesList(channelId) {
channelPermissionsRolesList.innerHTML = '';
if (channelPermissionsData.length === 0) {
channelPermissionsRolesList.innerHTML = 'No overrides configured.
';
return;
}
const sortedData = [...channelPermissionsData].sort((a, b) => {
if (a.type !== b.type) return a.type === 'role' ? -1 : 1;
const nameA = (a.display_name || '').toLowerCase();
const nameB = (b.display_name || '').toLowerCase();
const isAEveryone = nameA.includes('everyone');
const isBEveryone = nameB.includes('everyone');
if (isAEveryone && !isBEveryone) return -1;
if (!isAEveryone && isBEveryone) return 1;
return nameA.localeCompare(nameB);
});
sortedData.forEach(p => {
const item = document.createElement('div');
const isActive = currentSelectedOverrideRole == (p.type === 'role' ? p.role_id : p.user_id) && currentSelectedOverrideType === p.type;
item.className = `list-group-item list-group-item-action bg-transparent text-white border-0 mb-1 p-2 small d-flex align-items-center ${isActive ? 'active' : ''}`;
item.style.cursor = 'pointer';
let icon = '';
if (p.type === 'role') {
icon = `
`;
} else {
icon = ` `;
}
item.innerHTML = `
${icon}
${p.display_name}
`;
item.onclick = () => selectOverrideItem(p.type === 'role' ? p.role_id : p.user_id, p.display_name, p.type);
channelPermissionsRolesList.appendChild(item);
});
}
let currentSelectedOverrideType = 'role';
function selectOverrideItem(id, name, type) {
currentSelectedOverrideRole = id;
currentSelectedOverrideType = type;
const channelId = document.getElementById('edit-channel-id').value;
renderRoleOverridesList(channelId);
selectedPermRoleName.textContent = name;
noRoleSelectedView.classList.add('d-none');
channelPermissionsSettings.classList.remove('d-none');
const p = channelPermissionsData.find(perm => {
if (type === 'role') return perm.role_id == id && perm.type === 'role';
return perm.user_id == id && perm.type === 'member';
}) || { allow_permissions: 0, deny_permissions: 0 };
document.querySelectorAll('.perm-tri-state').forEach(group => {
const bit = parseInt(group.dataset.permBit);
updateToggleUI(bit, p.allow_permissions, p.deny_permissions);
});
}
function updateToggleUI(bit, allowPerms, denyPerms) {
const group = document.querySelector(`.perm-tri-state[data-perm-bit="${bit}"]`);
if (!group) return;
if (allowPerms & bit) {
group.querySelector('input[value="allow"]').checked = true;
} else if (denyPerms & bit) {
group.querySelector('input[value="deny"]').checked = true;
} else {
group.querySelector('input[value="neutral"]').checked = true;
}
}
removeSelectedPermRole?.addEventListener('click', async () => {
if (!currentSelectedOverrideRole) return;
const channelId = document.getElementById('edit-channel-id').value;
const payload = { channel_id: channelId };
if (currentSelectedOverrideType === 'role') {
payload.role_id = currentSelectedOverrideRole;
} else {
payload.user_id = currentSelectedOverrideRole;
}
await fetch('api_v1_channel_permissions.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
currentSelectedOverrideRole = null;
channelPermissionsSettings.classList.add('d-none');
noRoleSelectedView.classList.remove('d-none');
loadChannelPermissions(channelId);
});
permissionsTogglesContainer?.addEventListener('change', async (e) => {
if (e.target.type === 'radio') {
const group = e.target.closest('.perm-tri-state');
const bit = parseInt(group.dataset.permBit);
const val = e.target.value;
const channelId = document.getElementById('edit-channel-id').value;
const id = currentSelectedOverrideRole;
const type = currentSelectedOverrideType;
let p = channelPermissionsData.find(perm => {
if (type === 'role') return perm.role_id == id && perm.type === 'role';
return perm.user_id == id && perm.type === 'member';
});
if (!p) {
p = { channel_id: channelId, allow_permissions: 0, deny_permissions: 0, type: type };
if (type === 'role') p.role_id = id; else p.user_id = id;
channelPermissionsData.push(p);
}
let allow = parseInt(p.allow_permissions);
let deny = parseInt(p.deny_permissions);
// Clear current bit
allow &= ~bit;
deny &= ~bit;
if (val === 'allow') allow |= bit;
if (val === 'deny') deny |= bit;
const payload = { channel_id: channelId, allow, deny };
if (type === 'role') payload.role_id = id; else payload.user_id = id;
await fetch('api_v1_channel_permissions.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
// Update local data
p.allow_permissions = allow;
p.deny_permissions = deny;
}
});
document.addEventListener('click', async (e) => {
if (!e.target.closest('.search-container')) {
searchResults.style.display = 'none';
}
if (e.target.classList.contains('move-rule-btn')) {
if (!window.canManageChannels) return;
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';
const channelName = btn.dataset.name;
modal.querySelector('#edit-channel-id').value = channelId;
modal.querySelector('#edit-channel-name').value = channelName;
modal.querySelector('#header-channel-name').textContent = channelName;
modal.querySelector('#edit-channel-type').value = channelType;
// Force switch to Overview tab
const overviewTabBtn = modal.querySelector('[data-bs-target="#edit-channel-general"]');
if (overviewTabBtn) {
bootstrap.Tab.getOrCreateInstance(overviewTabBtn).show();
}
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-icon').value = btn.dataset.icon || '';
modal.querySelector('#edit-channel-rules-role').value = btn.dataset.rulesRole || '';
modal.querySelector('#edit-channel-category-id').value = btn.dataset.category || '';
modal.querySelector('#delete-channel-id').value = channelId;
// Check if channel is named "rรดle" or "role"
const isRoleChannel = channelName.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "") === "role";
// Toggle rules role visibility
const rulesRoleContainer = document.getElementById('edit-channel-rules-role-container');
if (rulesRoleContainer) {
rulesRoleContainer.style.display = (channelType === 'rules') ? 'block' : 'none';
}
// Hide limit, files and clear chat for rules, autorole, and role channels
const editLimitContainer = document.getElementById('edit-channel-limit-container');
const editFilesContainer = document.getElementById('edit-channel-files-container');
const clearChatBtn = document.getElementById('clear-channel-history-btn');
const hideExtra = (channelType === 'rules' || channelType === 'autorole' || isRoleChannel);
if (editLimitContainer) editLimitContainer.style.display = hideExtra ? 'none' : 'block';
if (editFilesContainer) editFilesContainer.style.display = hideExtra ? 'none' : 'block';
if (clearChatBtn) clearChatBtn.style.display = (channelType === 'rules') ? 'none' : 'inline-block';
// Reset delete zone
document.getElementById('delete-confirm-zone').style.display = 'none';
// 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
const rssTabBtn = document.getElementById('rss-tab-btn');
if (rssTabBtn && rssTabBtn.classList.contains('active')) {
bootstrap.Tab.getOrCreateInstance(modal.querySelector('[data-bs-target="#edit-channel-general"]')).show();
}
}
if (channelType === 'voice') {
statusContainer.style.display = 'block';
} else {
statusContainer.style.display = 'none';
}
});
});
document.getElementById('delete-channel-trigger')?.addEventListener('click', () => {
document.getElementById('delete-confirm-zone').style.display = 'block';
});
// 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');
const channelName = document.getElementById('edit-channel-name').value;
const isRoleChannel = channelName.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "") === "role";
rssTabNav.style.display = (type === 'announcement') ? 'block' : 'none';
statusContainer.style.display = (type === 'voice') ? 'block' : 'none';
// Rules specific visibility
const rulesRoleContainer = document.getElementById('edit-channel-rules-role-container');
if (rulesRoleContainer) {
rulesRoleContainer.style.display = (type === 'rules') ? 'block' : 'none';
}
const editLimitContainer = document.getElementById('edit-channel-limit-container');
const editFilesContainer = document.getElementById('edit-channel-files-container');
const clearChatBtn = document.getElementById('clear-channel-history-btn');
const hideExtra = (type === 'rules' || type === 'autorole' || isRoleChannel);
if (editLimitContainer) editLimitContainer.style.display = hideExtra ? 'none' : 'block';
if (editFilesContainer) editFilesContainer.style.display = hideExtra ? 'none' : 'block';
if (clearChatBtn) clearChatBtn.style.display = (type === 'rules') ? 'none' : 'inline-block';
});
// 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();
}
});
// Auto-sync for announcement channels
if (window.activeChannelId) {
const autoSyncRss = async () => {
// Check if we are in an announcement channel
// We can look for the bullhorn icon in the header
const headerIcon = document.querySelector('.chat-header i.fa-bullhorn');
if (headerIcon) {
const formData = new FormData();
formData.append('action', 'sync');
formData.append('channel_id', window.activeChannelId);
formData.append('auto', '1');
try {
const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData });
const data = await resp.json();
if (data.success && data.new_items > 0) {
// If new items were found, we might want to refresh the message list
// but only if we are still on the same channel
if (window.activeChannelId == data.channel_id) {
// For now, we don't reload automatically to avoid interrupting the user
// The new messages will appear on next reload or polling if implemented
}
}
} catch (e) { }
}
};
// Initial sync
setTimeout(autoSyncRss, 2000);
// Periodic sync every 2 minutes
setInterval(autoSyncRss, 120000);
}
// 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 = window.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() {
try {
const channelIdParam = window.activeChannelId ? `&channel_id=${window.activeChannelId}` : '';
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}${channelIdParam}`);
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);
// Use filtered members for sidebar, all members for colors
updateGlobalUI(data.filtered_members || data.members, data.members);
}
} catch (e) { console.error(e); }
}
function renderRoleIconJS(icon, size = '14px') {
if (!icon) return '';
const isUrl = icon.startsWith('http') || icon.startsWith('/');
if (isUrl) {
return ` `;
} else if (icon.startsWith(':') && icon.endsWith(':')) {
const ce = (window.CUSTOM_EMOTES_CACHE || []).find(e => e.code === icon);
if (ce) {
return ` `;
}
return `${escapeHTML(icon)} `;
} else {
return `${escapeHTML(icon)} `;
}
}
function updateGlobalUI(sidebarMembers, allMembers = null) {
if (!allMembers) allMembers = sidebarMembers;
// Update members sidebar
const sidebar = document.querySelector('.members-sidebar');
if (sidebar) {
const countEl = sidebar.querySelector('div[style*="text-transform: uppercase"]');
if (countEl) countEl.textContent = `Members โ ${sidebarMembers.length}`;
// We need to keep the "Members - X" div and replace everything else
const header = sidebar.firstElementChild;
sidebar.innerHTML = '';
sidebar.appendChild(header);
sidebarMembers.forEach(m => {
const item = document.createElement('div');
item.className = 'channel-item member-item';
item.dataset.userId = m.id;
item.dataset.username = m.username;
item.dataset.avatar = m.avatar_url || '';
item.dataset.roleIds = m.role_ids || '';
item.style.color = 'var(--text-primary)';
item.style.marginBottom = '8px';
item.style.cursor = 'pointer';
const roleIconHtml = renderRoleIconJS(m.role_icon, '14px');
const avatarBg = m.avatar_url ? `background-image: url('${m.avatar_url}');` : '';
const statusColor = m.status === 'online' ? '#23a559' : '#80848e';
item.innerHTML = `
${m.status === 'online' ? `
` : ''}
${escapeHTML(m.username)}
${roleIconHtml}
`;
sidebar.appendChild(item);
});
}
// Update chat colors
document.querySelectorAll('.message-author').forEach(authorEl => {
const username = authorEl.childNodes[0].textContent.trim();
const member = allMembers.find(m => m.username === username);
if (member) {
authorEl.style.color = member.role_color || 'inherit';
// Try to update icon if it exists or add it if it doesn't
let iconEl = authorEl.querySelector('.role-icon, span.ms-1');
const newIconHtml = renderRoleIconJS(member.role_icon, '14px');
if (newIconHtml) {
if (iconEl) {
const temp = document.createElement('div');
temp.innerHTML = newIconHtml;
iconEl.replaceWith(temp.firstChild);
} else {
const temp = document.createElement('div');
temp.innerHTML = newIconHtml;
// Insert after the text node
authorEl.insertBefore(temp.firstChild, authorEl.childNodes[1]);
}
} else if (iconEl) {
iconEl.remove();
}
}
});
}
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 role-sortable-item';
item.dataset.id = role.id;
const roleIconHtml = renderRoleIconJS(role.icon_url, '14px');
item.innerHTML = `
${role.name}
${roleIconHtml}
Edit
ร
`;
rolesList.appendChild(item);
});
// Initialize Sortable for roles
if (typeof Sortable !== 'undefined' && rolesList) {
new Sortable(rolesList, {
animation: 150,
handle: '.role-drag-handle',
ghostClass: 'sortable-ghost',
onEnd: () => saveRolePositions()
});
}
}
async function saveRolePositions() {
const orders = [];
const items = rolesList.querySelectorAll('.role-sortable-item');
// Invert the order because we ORDER BY position DESC in SQL
let position = items.length - 1;
items.forEach(item => {
orders.push({
id: item.dataset.id,
position: position--
});
});
try {
await fetch('api_v1_roles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'reorder',
server_id: activeServerId,
orders: orders
})
});
} catch (e) { console.error(e); }
}
function renderMembers(members) {
membersList.innerHTML = '';
members.forEach(member => {
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';
const roleIconHtml = renderRoleIconJS(member.role_icon, '14px');
item.innerHTML = `
${escapeHTML(member.username)}
${roleIconHtml}
${member.role_names ? member.role_names.split(',').join(', ') : 'No roles'}
${(window.isServerOwner || window.canManageServer) ? `
Roles
` : ''}
`;
membersList.appendChild(item);
});
}
// Add listener for the button in members list tab
membersList?.addEventListener('click', (e) => {
const btn = e.target.closest('.edit-user-roles-settings-btn');
if (btn) {
openEditUserRolesModal(btn.dataset.id, btn.dataset.username, btn.dataset.avatar);
}
});
// 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;
document.getElementById('edit-role-icon').value = role.icon;
document.getElementById('selected-role-emoji-preview').textContent = role.icon || '';
const modalTitle = document.querySelector('#roleEditorModal .modal-title');
if (modalTitle) modalTitle.textContent = 'Modifier le rรดle';
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 += `
${p.name}
`;
});
const modal = new bootstrap.Modal(document.getElementById('roleEditorModal'));
modal.show();
}
});
document.getElementById('save-role-btn')?.addEventListener('click', async (e) => {
const btn = e.target;
const originalText = btn.textContent;
const id = document.getElementById('edit-role-id').value;
const name = document.getElementById('edit-role-name').value;
const color = document.getElementById('edit-role-color').value;
const icon_url = document.getElementById('edit-role-icon').value;
let permissions = 0;
document.querySelectorAll('.perm-check:checked').forEach(cb => {
permissions |= parseInt(cb.value);
});
try {
const action = id ? 'update' : 'create';
const resp = await fetch('api_v1_roles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, server_id: activeServerId, id, name, color, icon_url, permissions })
});
const data = await resp.json();
if (data.success) {
btn.textContent = 'Saved โ
';
btn.classList.replace('btn-primary', 'btn-success');
setTimeout(() => {
btn.textContent = originalText;
btn.classList.replace('btn-success', 'btn-primary');
}, 2000);
loadRoles();
}
} catch (e) { console.error(e); }
});
async function openEditUserRolesModal(userId, username, avatar) {
const modal = document.getElementById('editUserRolesModal');
document.getElementById('edit-user-roles-user-id').value = userId;
document.getElementById('edit-user-roles-username').textContent = username;
const avatarEl = document.getElementById('edit-user-roles-avatar');
avatarEl.style.backgroundImage = avatar ? `url('${avatar}')` : 'none';
const list = document.getElementById('user-roles-selection-list');
list.innerHTML = 'Loading roles...
';
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
try {
// We need to fetch roles and the current user's roles
// We can reuse loadRoles or make a specific call
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
const data = await resp.json();
if (data.success) {
const member = data.members.find(m => m.id == userId);
const assignedRoles = member && member.role_ids ? member.role_ids.split(',') : [];
list.innerHTML = '';
// Sort roles by position descending for display
data.roles.sort((a, b) => b.position - a.position).forEach(role => {
const isChecked = assignedRoles.includes(role.id.toString());
const item = document.createElement('div');
item.className = 'list-group-item bg-dark text-white border-secondary p-2 d-flex align-items-center';
item.innerHTML = `
${role.name}
`;
list.appendChild(item);
});
if (data.roles.length === 0) {
list.innerHTML = 'No roles defined for this server.
';
}
}
} catch (e) { console.error(e); }
}
document.getElementById('save-user-roles-btn')?.addEventListener('click', async (e) => {
const btn = e.target;
const originalText = btn.textContent;
const userId = document.getElementById('edit-user-roles-user-id').value;
const roleIds = Array.from(document.querySelectorAll('.user-role-checkbox:checked')).map(cb => cb.value);
try {
const resp = await fetch('api_v1_roles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'set_user_roles',
server_id: activeServerId,
user_id: userId,
role_ids: roleIds
})
});
const data = await resp.json();
if (data.success) {
btn.textContent = 'Saved โ
';
btn.classList.replace('btn-primary', 'btn-success');
setTimeout(() => {
btn.textContent = originalText;
btn.classList.replace('btn-success', 'btn-primary');
}, 2000);
loadRoles();
} else {
alert(data.error || 'Failed to update roles');
}
} catch (e) { console.error(e); }
});
addRoleBtn?.addEventListener('click', async () => {
document.getElementById('edit-role-id').value = '';
document.getElementById('edit-role-name').value = 'New Role';
document.getElementById('edit-role-color').value = '#99aab5';
document.getElementById('edit-role-icon').value = '';
document.getElementById('selected-role-emoji-preview').textContent = '';
const modalTitle = document.querySelector('#roleEditorModal .modal-title');
if (modalTitle) modalTitle.textContent = 'Crรฉer un rรดle';
const permsContainer = document.getElementById('role-permissions-checkboxes');
permsContainer.innerHTML = '';
serverPermissions.forEach(p => {
permsContainer.innerHTML += `
${p.name}
`;
});
const modal = new bootstrap.Modal(document.getElementById('roleEditorModal'));
modal.show();
});
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}
Copy
`;
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();
});
// Forum Thread Actions (Pin/Lock)
const pinThreadBtn = document.getElementById('toggle-pin-thread');
pinThreadBtn?.addEventListener('click', async () => {
const id = pinThreadBtn.dataset.id;
const pinned = pinThreadBtn.dataset.pinned == '1';
const action = pinned ? 'unpin' : 'pin';
try {
const resp = await fetch(`api_v1_threads.php?action=${action}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ thread_id: id })
});
const result = await resp.json();
if (result.success) location.reload();
else alert(result.error || 'Failed to update thread');
} catch (e) { console.error(e); }
});
const lockThreadBtn = document.getElementById('toggle-lock-thread');
lockThreadBtn?.addEventListener('click', async () => {
const id = lockThreadBtn.dataset.id;
const locked = lockThreadBtn.dataset.locked == '1';
const action = locked ? 'unlock' : 'lock';
try {
const resp = await fetch(`api_v1_threads.php?action=${action}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ thread_id: id })
});
const result = await resp.json();
if (result.success) location.reload();
else alert(result.error || 'Failed to update thread');
} catch (e) { console.error(e); }
});
const deleteThreadBtn = document.getElementById('delete-thread-btn');
deleteThreadBtn?.addEventListener('click', async () => {
if (!confirm('Are you sure you want to delete this thread? This action cannot be undone.')) return;
const id = deleteThreadBtn.dataset.id;
const channelId = deleteThreadBtn.dataset.channelId;
const serverId = deleteThreadBtn.dataset.serverId;
try {
const resp = await fetch(`api_v1_threads.php?action=delete`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ thread_id: id })
});
const result = await resp.json();
if (result.success) {
location.href = `?server_id=${serverId}&channel_id=${channelId}`;
} else {
alert(result.error || 'Failed to delete thread');
}
} catch (e) { console.error(e); }
});
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');
const addRuleForm = document.getElementById('add-rule-form');
const newRuleContent = document.getElementById('new-rule-content');
const saveNewRuleBtn = document.getElementById('save-new-rule-btn');
const cancelNewRuleBtn = document.getElementById('cancel-new-rule-btn');
addRuleBtn?.addEventListener('click', () => {
addRuleBtn.style.display = 'none';
addRuleForm.style.display = 'block';
newRuleContent.focus();
});
cancelNewRuleBtn?.addEventListener('click', () => {
addRuleBtn.style.display = 'block';
addRuleForm.style.display = 'none';
newRuleContent.value = '';
});
saveNewRuleBtn?.addEventListener('click', async () => {
const content = newRuleContent.value.trim();
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); }
});
const rulesListSortable = document.getElementById('rules-list-sortable');
if (typeof Sortable !== 'undefined' && rulesListSortable && window.canManageChannels) {
new Sortable(rulesListSortable, {
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: async () => {
const order = Array.from(rulesListSortable.querySelectorAll('.rule-item')).map(el => el.dataset.id);
// Update numbers in UI
rulesListSortable.querySelectorAll('.rule-item').forEach((item, index) => {
const numEl = item.querySelector('.rule-number');
if (numEl) numEl.textContent = `${index + 1}.`;
});
await fetch('api_v1_rules.php', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order: order })
});
}
});
}
// 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();
}
});
// Rules Acceptance
document.getElementById('accept-rules-btn')?.addEventListener('click', async () => {
const btn = document.getElementById('accept-rules-btn');
btn.disabled = true;
btn.innerHTML = ' Traitement...';
try {
const resp = await fetch('api_v1_accept_rules.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel_id: window.activeChannelId })
});
const data = await resp.json();
if (data.success) {
const container = document.getElementById('rules-acceptance-container');
container.innerHTML = ' Vous avez acceptรฉ les rรจgles.
';
// Reload roles in members list if possible, or just reload page
setTimeout(() => location.reload(), 1500);
} else {
alert(data.error || 'Erreur lors de l\'acceptation');
btn.disabled = false;
btn.innerHTML = ' J\'accepte les rรจgles';
}
} catch (e) {
console.error(e);
btn.disabled = false;
btn.innerHTML = ' J\'accepte les rรจgles';
}
});
document.getElementById('withdraw-rules-btn')?.addEventListener('click', async () => {
if (!confirm('รtes-vous sรปr de vouloir retirer votre acceptation des rรจgles ? Vous perdrez le rรดle associรฉ.')) return;
const btn = document.getElementById('withdraw-rules-btn');
btn.disabled = true;
btn.innerHTML = ' Traitement...';
try {
const resp = await fetch('api_v1_withdraw_rules.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel_id: window.activeChannelId })
});
const data = await resp.json();
if (data.success) {
location.reload();
} else {
alert(data.error || 'Erreur lors du retrait');
btn.disabled = false;
btn.innerHTML = ' Retirer mon acceptation';
}
} catch (e) {
console.error(e);
btn.disabled = false;
btn.innerHTML = ' Retirer mon acceptation';
}
});
// 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('add-channel-type'); // Corrected ID from index.php
if (select) {
select.value = type === 'voice' ? 'voice' : (type || 'chat');
// Trigger change to update visibility
select.dispatchEvent(new Event('change'));
}
});
});
const addChannelTypeSelect = document.getElementById('add-channel-type');
addChannelTypeSelect?.addEventListener('change', (e) => {
const type = e.target.value;
const container = document.getElementById('add-channel-rules-role-container');
if (container) {
container.style.display = (type === 'rules') ? 'block' : 'none';
}
const limitContainer = document.getElementById('add-channel-limit-container');
const filesContainer = document.getElementById('add-channel-files-container');
if (limitContainer) limitContainer.style.display = (type === 'rules' || type === 'autorole') ? 'none' : 'block';
if (filesContainer) filesContainer.style.display = (type === 'rules' || type === 'autorole') ? 'none' : 'block';
});
// User Settings - Avatar Search
// User Settings - Save logic removed and moved to index.php for reliability
const avatarSearchBtn = document.getElementById('search-avatar-btn');
const avatarRefreshBtn = document.getElementById('refresh-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');
const avatarUploadInput = document.getElementById('avatar-upload-input');
avatarUploadInput?.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('avatar', file);
try {
avatarPreview.innerHTML = '
';
const resp = await fetch('api/upload_avatar.php', {
method: 'POST',
body: formData
});
const data = await resp.json();
avatarPreview.innerHTML = '';
if (data.success) {
avatarUrlInput.value = data.url;
avatarPreview.style.backgroundImage = `url('${data.url}')`;
} else {
alert(data.error || 'Erreur lors de l\'upload');
}
} catch (err) {
console.error(err);
avatarPreview.innerHTML = '';
alert('Erreur rรฉseau lors de l\'upload');
}
});
const serverIconUploadInput = document.getElementById('server-icon-upload-input');
// serverIconPreview and serverIconUrlInput are already declared above
serverIconUploadInput?.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('icon', file);
formData.append('server_id', window.activeServerId);
try {
serverIconPreview.innerHTML = '
';
const resp = await fetch('api/upload_server_icon.php', {
method: 'POST',
body: formData
});
const data = await resp.json();
serverIconPreview.innerHTML = '';
if (data.success) {
serverIconUrlInput.value = data.url;
serverIconPreview.style.backgroundImage = `url('${data.url}')`;
} else {
alert(data.error || 'Erreur lors de l\'upload');
}
} catch (err) {
console.error(err);
serverIconPreview.innerHTML = '';
alert('Erreur rรฉseau lors de l\'upload');
}
});
let currentAvatarPage = 1;
async function fetchAvatars(q, page = 1) {
if (!q) return;
avatarResults.innerHTML = 'Searching...
';
try {
const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(q)}&page=${page}`);
const data = await resp.json();
avatarResults.innerHTML = '';
if (data && Array.isArray(data)) {
data.forEach(photo => {
const img = document.createElement('img');
img.src = photo.url;
img.className = 'avatar-pick';
img.style.width = '100%';
img.style.height = 'auto';
img.style.aspectRatio = '1/1';
img.style.objectFit = 'cover';
img.style.borderRadius = '4px';
img.style.cursor = 'pointer';
img.onclick = () => {
avatarUrlInput.value = photo.url;
avatarPreview.style.backgroundImage = `url('${photo.url}')`;
};
avatarResults.appendChild(img);
});
} else {
avatarResults.innerHTML = 'Aucun rรฉsultat trouvรฉ.
';
}
} catch (e) {
console.error(e);
avatarResults.innerHTML = 'Erreur lors de la rรฉcupรฉration.
';
}
}
avatarSearchBtn?.addEventListener('click', () => {
currentAvatarPage = 1;
fetchAvatars(avatarSearchQuery.value.trim(), currentAvatarPage);
});
avatarRefreshBtn?.addEventListener('click', () => {
currentAvatarPage++;
fetchAvatars(avatarSearchQuery.value.trim() || 'avatar', currentAvatarPage);
});
avatarSearchQuery?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
currentAvatarPage = 1;
fetchAvatars(avatarSearchQuery.value.trim(), currentAvatarPage);
}
});
// Theme preview
document.querySelectorAll('input[name="theme"]').forEach(radio => {
radio.addEventListener('change', (e) => {
document.body.setAttribute('data-theme', e.target.value);
});
});
// 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 handled in index.php
function escapeHTML(str) {
if (!str) return "";
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function parseMarkdown(text) {
if (!text) return "";
// Escape HTML first
let html = escapeHTML(text);
// Code blocks: ```language\ncontent```
const codeBlocks = [];
html = html.replace(/```(?:(\w+)\n)?([\s\S]*?)```/g, (match, lang, content) => {
const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`;
codeBlocks.push(`${content} `);
return placeholder;
});
// Inline code: `content`
const inlineCodes = [];
html = html.replace(/`([^`\n]+)`/g, (match, content) => {
const placeholder = `__INLINE_CODE_${inlineCodes.length}__`;
inlineCodes.push(`${content}`);
return placeholder;
});
// Bold: **text**
html = html.replace(/\*\*([^*]+)\*\*/g, '$1 ');
// Italics: *text* or _text_
html = html.replace(/\*([^*]+)\*/g, '$1 ');
html = html.replace(/_([^_]+)_/g, '$1 ');
// Underline: __text__
html = html.replace(/__([^_]+)__/g, '$1 ');
// Strikethrough: ~~text~~
html = html.replace(/~~([^~]+)~~/g, '$1');
// Spoiler: ||text||
html = html.replace(/\|\|([^|]+)\|\|/g, '$1 ');
// Headers: # H1, ## H2, ### H3 (must be at start of line)
html = html.replace(/^# (.*$)/gm, '$1 ');
html = html.replace(/^## (.*$)/gm, '$1 ');
html = html.replace(/^### (.*$)/gm, '$1 ');
// Subtext: -# text (must be at start of line)
html = html.replace(/^-# (.*$)/gm, '$1 ');
// Blockquotes: > text or >>> text (must be at start of line)
html = html.replace(/^> (.*$)/gm, '$1 ');
html = html.replace(/^>>> ([\s\S]*$)/g, '$1 ');
// Hyperlinks: [text](url)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ');
// Pure links:
html = html.replace(/<(https?:\/\/[^&]+)>/g, '$1 ');
// Newlines to (only those not inside placeholders)
html = html.replace(/\n/g, ' ');
// Remove extra space around headers and blockquotes added by nl2br
html = html.replace(/( )\s*(|||)/gi, '$2');
html = html.replace(/(<\/h1>|<\/h2>|<\/h3>|<\/blockquote>)\s*( )/gi, '$1');
// Re-insert inline code
inlineCodes.forEach((code, i) => {
html = html.replace(`__INLINE_CODE_${i}__`, code);
});
// Re-insert code blocks
codeBlocks.forEach((block, i) => {
html = html.replace(`__CODE_BLOCK_${i}__`, block);
});
return html;
}
function parseCustomEmotes(text) {
let parsed = parseMarkdown(text);
(window.CUSTOM_EMOTES_CACHE || []).forEach(emote => {
const imgHtml = ` `;
// Only replace if it's not inside a tag attribute or code block (simplified)
parsed = parsed.split(emote.code).join(imgHtml);
});
return parsed;
}
function appendMessage(msg) {
if (!msg || !msg.id) return;
if (document.querySelector(`.message-item[data-id="${msg.id}"]`)) return;
// Security: Ensure message belongs to current channel/thread
if (msg.channel_id && msg.channel_id != currentChannel) return;
if (window.activeChannelType === 'forum') {
if (!currentThread || msg.thread_id != currentThread) return;
} else {
if (msg.thread_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 messagesInner = document.querySelector('.messages-list-inner');
const targetContainer = messagesInner || messagesList;
if (!targetContainer) return;
const div = document.createElement('div');
div.className = 'message-item';
div.dataset.id = msg.id;
div.dataset.rawContent = msg.content;
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');
const mentionHtml = `@${window.currentUsername} `;
const contentWithMentions = parseCustomEmotes(msg.content).replace(mentionRegex, mentionHtml);
div.innerHTML = `
${contentWithMentions}
${attachmentHtml}
${embedHtml}
${actionsHtml}
`;
targetContainer.appendChild(div);
scrollToBottom(isMe);
// Ensure we scroll again when images/videos load
div.querySelectorAll('img, iframe').forEach(el => {
el.addEventListener('load', () => scrollToBottom(isMe));
});
}
// Initial load of roles for the server
loadRoles();
// Autorole Toggle
document.addEventListener('click', async (e) => {
const btn = e.target.closest('.autorole-toggle-btn');
if (!btn) return;
const roleId = btn.dataset.roleId;
btn.disabled = true;
try {
const resp = await fetch('api_v1_autoroles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'toggle', role_id: roleId })
});
const data = await resp.json();
if (data.success) {
if (data.added) {
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-primary');
btn.style.backgroundColor = 'var(--blurple)';
btn.style.border = 'none';
} else {
btn.classList.add('btn-outline-secondary');
btn.classList.remove('btn-primary');
btn.style.backgroundColor = 'var(--bg-channels)';
btn.style.border = '1px solid var(--separator)';
}
// Refresh sidebar channels and members
if (typeof loadRoles === 'function') loadRoles();
fetch(window.location.href)
.then(resp => resp.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newSidebar = doc.getElementById('sidebar-channels-list');
const currentSidebar = document.getElementById('sidebar-channels-list');
if (newSidebar && currentSidebar) {
currentSidebar.innerHTML = newSidebar.innerHTML;
if (window.restoreCollapsedStates) window.restoreCollapsedStates();
}
})
.catch(err => console.error('Error refreshing sidebar:', err));
} else {
alert(data.error || 'Failed to toggle role');
}
} catch (e) {
console.error(e);
} finally {
btn.disabled = false;
}
});
// Category Collapsible Logic
document.addEventListener('click', (e) => {
const categoryHeader = e.target.closest('.channel-category');
if (categoryHeader) {
// Check if we didn't click on a settings button or add button
if (e.target.closest('.channel-settings-btn') || e.target.closest('.add-channel-btn')) {
return;
}
const wrapper = categoryHeader.closest('.category-wrapper');
if (wrapper) {
wrapper.classList.toggle('collapsed');
// Persist state in localStorage
const categoryId = wrapper.dataset.id;
const collapsedStates = JSON.parse(localStorage.getItem('categoryCollapsedStates') || '{}');
collapsedStates[categoryId] = wrapper.classList.contains('collapsed');
localStorage.setItem('categoryCollapsedStates', JSON.stringify(collapsedStates));
}
}
});
// Restore collapsed states
window.restoreCollapsedStates = () => {
const collapsedStates = JSON.parse(localStorage.getItem('categoryCollapsedStates') || '{}');
Object.entries(collapsedStates).forEach(([id, isCollapsed]) => {
if (isCollapsed) {
const wrapper = document.querySelector(`.category-wrapper[data-id="${id}"]`);
if (wrapper) {
wrapper.classList.add('collapsed');
}
}
});
};
restoreCollapsedStates();
// Invite code refresh and timer
const refreshBtn = document.getElementById('refresh-invite-code-btn');
const inviteInput = document.getElementById('server-invite-code');
const timerContainer = document.getElementById('invite-code-timer');
if (refreshBtn) {
refreshBtn.addEventListener('click', async () => {
const formData = new FormData();
formData.append('server_id', window.activeServerId);
try {
const resp = await fetch('api/refresh_invite_code.php', {
method: 'POST',
body: formData
});
const data = await resp.json();
if (data.success) {
if (inviteInput) inviteInput.value = data.invite_code;
if (timerContainer) {
timerContainer.dataset.expires = data.expires_at;
timerContainer.innerHTML = 'Expires in: 30:00 ';
}
} else {
alert('Error: ' + data.error);
}
} catch (e) {
console.error(e);
alert('Failed to refresh invite code.');
}
});
}
function updateInviteTimer() {
const display = document.getElementById('invite-timer-display');
const container = document.getElementById('invite-code-timer');
if (!display || !container || !container.dataset.expires) return;
const expiresAt = new Date(container.dataset.expires).getTime();
const now = new Date().getTime();
const diff = expiresAt - now;
if (diff <= 0) {
container.innerHTML = 'Expired ';
return;
}
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
display.innerText = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
if (timerContainer) {
setInterval(updateInviteTimer, 1000);
updateInviteTimer();
}
});