3317 lines
162 KiB
JavaScript
3317 lines
162 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
||
const fileUpload = document.getElementById('file-upload');
|
||
const chatForm = document.getElementById('chat-form');
|
||
const chatInput = document.getElementById('chat-input');
|
||
const messagesList = document.getElementById('messages-list');
|
||
const typingIndicator = document.getElementById('typing-indicator');
|
||
|
||
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 = '<div class="col-12 text-center p-4"><div class="spinner-border spinner-border-sm text-primary"></div></div>';
|
||
|
||
if (category === 'Custom' && !searchTerm) {
|
||
if (uploadZone) uploadZone.classList.remove('d-none');
|
||
const emotes = await window.loadCustomEmotes();
|
||
grid.innerHTML = '';
|
||
|
||
if (emotes.length === 0) {
|
||
grid.innerHTML = '<div class="col-12 text-center text-muted p-4" style="grid-column: 1 / -1;">Aucune emote personnalisรฉe. Ajoutez-en une !</div>';
|
||
} 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 = `
|
||
<img src="${emote.path}" style="width: 32px; height: 32px; object-fit: contain;">
|
||
<small class="text-white mt-1" style="font-size: 10px; opacity: 0.7;">${emote.code}</small>
|
||
<div class="emote-actions position-absolute top-0 end-0 p-1 d-none">
|
||
<button class="btn btn-sm btn-link text-info p-0 me-1 edit-emote" title="Renommer"><i class="fas fa-edit" style="font-size: 10px;"></i></button>
|
||
<button class="btn btn-sm btn-link text-danger p-0 delete-emote" title="Supprimer"><i class="fas fa-trash" style="font-size: 10px;"></i></button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `<i class="fas ${cat === 'Custom' ? 'fa-star' : 'fa-smile'} opacity-75"></i> ${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 = `<img src="${emote.path}" style="width: 24px; height: 24px; object-fit: contain;" title="${emote.code}">`;
|
||
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 = `<img src="${ce.path}" style="width: 32px; height: 32px; object-fit: contain;">`;
|
||
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} <span class="count">${r.count}</span>`;
|
||
container.appendChild(badge);
|
||
});
|
||
container.appendChild(addBtn);
|
||
}
|
||
|
||
function updatePresenceUI(userId, status) {
|
||
const memberItem = document.querySelector(`.start-dm-btn[data-user-id="${userId}"] .message-avatar`);
|
||
if (memberItem) {
|
||
let indicator = memberItem.querySelector('.presence-indicator');
|
||
if (!indicator) {
|
||
indicator = document.createElement('div');
|
||
indicator.className = 'presence-indicator';
|
||
memberItem.appendChild(indicator);
|
||
}
|
||
indicator.style.position = 'absolute';
|
||
indicator.style.bottom = '0';
|
||
indicator.style.right = '0';
|
||
indicator.style.width = '10px';
|
||
indicator.style.height = '10px';
|
||
indicator.style.borderRadius = '50%';
|
||
indicator.style.border = '2px solid var(--bg-members)';
|
||
indicator.style.backgroundColor = status === 'online' ? '#23a559' : '#80848e';
|
||
}
|
||
}
|
||
|
||
// 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 = '<div class="p-3 text-center text-muted">Loading pinned messages...</div>';
|
||
const modal = new bootstrap.Modal(document.getElementById('pinnedMessagesModal'));
|
||
modal.show();
|
||
|
||
const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&pinned=1`);
|
||
const data = await resp.json();
|
||
if (data.success && data.messages.length > 0) {
|
||
container.innerHTML = '';
|
||
data.messages.forEach(msg => {
|
||
const div = document.createElement('div');
|
||
div.className = 'message-item p-2 border-bottom border-secondary';
|
||
div.dataset.id = msg.id;
|
||
div.dataset.rawContent = msg.content;
|
||
div.style.backgroundColor = 'transparent';
|
||
const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
|
||
div.innerHTML = `
|
||
<div class="d-flex align-items-start">
|
||
<div class="message-avatar" style="width: 32px; height: 32px; margin-right: 10px; ${msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : ''}"></div>
|
||
<div style="flex: 1;">
|
||
<div class="message-author" style="font-size: 0.85em; ${authorStyle}">
|
||
${escapeHTML(msg.username)}
|
||
${renderRoleIconJS(msg.role_icon, '14px')}
|
||
<span class="message-time">${msg.time}</span>
|
||
</div>
|
||
<div class="message-text" style="font-size: 0.9em;">
|
||
${parseCustomEmotes(msg.content)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
container.appendChild(div);
|
||
});
|
||
} else {
|
||
container.innerHTML = '<div class="p-3 text-center text-muted">No pinned messages in this channel.</div>';
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 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);
|
||
const badgeData = (memberItem.dataset.badgeData || '').split(':::').filter(d => d);
|
||
|
||
// 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 badgesHtml = '';
|
||
if (badgeData.length > 0) {
|
||
badgesHtml = `
|
||
<div class="mb-2 p-1">
|
||
<div class="small text-muted text-uppercase mb-1" style="font-size: 0.6em; font-weight: bold; opacity: 0.8;">Badges</div>
|
||
<div class="d-flex flex-wrap gap-1">
|
||
${badgeData.map(d => {
|
||
const parts = d.split('|');
|
||
const name = parts[0];
|
||
const url = parts[1];
|
||
return `<img src="${url}" style="width: 32px; height: 32px; object-fit: contain;" title="${escapeHTML(name)}">`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
<div class="border-top border-secondary mb-2 mx-1"></div>
|
||
`;
|
||
}
|
||
|
||
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 = `
|
||
<div class="mb-2 p-1">
|
||
<div class="small text-muted text-uppercase mb-1" style="font-size: 0.6em; font-weight: bold; opacity: 0.8;">Rรดles</div>
|
||
<div class="d-flex flex-wrap gap-1">
|
||
${roles.map(r => `
|
||
<span class="badge rounded-pill d-flex align-items-center" style="background-color: rgba(0,0,0,0.3); border: 1px solid ${r.color}; font-size: 0.7em; color: ${r.color}; font-weight: 500; padding: 2px 8px;">
|
||
${r.icon_url ? `<img src="${r.icon_url}" style="width: 14px; height: 14px; margin-right: 4px; object-fit: contain;">` : ''}
|
||
${escapeHTML(r.name)}
|
||
</span>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
<div class="border-top border-secondary mb-2 mx-1"></div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
menu.innerHTML = `
|
||
<div class="p-1 d-flex align-items-center mb-1">
|
||
<div class="message-avatar me-2" style="width: 24px; height: 24px; ${avatar ? `background-image: url('${avatar}');` : ''}"></div>
|
||
<span class="small fw-bold">${escapeHTML(username)}</span>
|
||
</div>
|
||
<div class="border-top border-secondary mb-2 mx-1"></div>
|
||
${badgesHtml}
|
||
${rolesHtml}
|
||
<button class="btn btn-sm btn-dark w-100 text-start mb-1 member-menu-action" data-action="message">Message</button>
|
||
${(window.isServerOwner || window.canManageServer) ? `
|
||
<button class="btn btn-sm btn-dark w-100 text-start mb-1 member-menu-action" data-action="edit-roles">รditer son rรดle</button>
|
||
<button class="btn btn-sm btn-dark w-100 text-start member-menu-action" data-action="edit-badges">รditer ses badges</button>
|
||
` : ''}
|
||
`;
|
||
|
||
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);
|
||
} else if (action === 'edit-badges') {
|
||
openEditUserBadgesModal(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 = `
|
||
<div class="message-avatar" style="width: 24px; height: 24px; ${res.avatar_url ? `background-image: url('${res.avatar_url}');` : ''}"></div>
|
||
<div class="flex-grow-1">
|
||
<div class="search-result-author">${res.username}</div>
|
||
<div class="small text-muted" style="font-size: 0.7em;">Click to start conversation</div>
|
||
</div>
|
||
`;
|
||
item.onclick = () => {
|
||
const formData = new FormData();
|
||
formData.append('user_id', res.id);
|
||
fetch('api_v1_dms.php', { method: 'POST', body: formData })
|
||
.then(r => r.json())
|
||
.then(resDM => {
|
||
if (resDM.success) window.location.href = `?server_id=dms&channel_id=${resDM.channel_id}`;
|
||
});
|
||
};
|
||
} else {
|
||
item.innerHTML = `
|
||
<div class="flex-grow-1">
|
||
<div class="search-result-author">${escapeHTML(res.username)}</div>
|
||
<div class="search-result-text">${parseCustomEmotes(res.content)}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
searchResults.appendChild(item);
|
||
});
|
||
searchResults.style.display = 'block';
|
||
} else {
|
||
searchResults.innerHTML = '<div class="p-2 text-muted">No results found</div>';
|
||
searchResults.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
// Channel Permissions Management
|
||
const channelPermissionsTabBtn = document.getElementById('channel-permissions-tab-btn');
|
||
const 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 = '<div class="text-center p-3 text-muted small">Loading...</div>';
|
||
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 = '<li><span class="dropdown-item text-muted">Loading...</span></li>';
|
||
|
||
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 = '<h6 class="dropdown-header text-uppercase" style="font-size: 0.65em; color: #949ba4;">Roles</h6>';
|
||
addPermRoleList.appendChild(header);
|
||
|
||
availableRoles.forEach(role => {
|
||
const li = document.createElement('li');
|
||
li.innerHTML = `<a class="dropdown-item d-flex align-items-center gap-2 py-2" href="#">
|
||
<div style="width: 14px; height: 14px; border-radius: 50%; background-color: ${role.color || '#99aab5'}; border: 1px solid var(--separator);"></div>
|
||
<span style="color: var(--text-primary); font-size: 0.9em;">${role.name}</span>
|
||
</a>`;
|
||
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 = '<h6 class="dropdown-header text-uppercase mt-2" style="font-size: 0.65em; color: #949ba4;">Members</h6>';
|
||
addPermRoleList.appendChild(header);
|
||
|
||
availableMembers.forEach(m => {
|
||
const li = document.createElement('li');
|
||
li.innerHTML = `<a class="dropdown-item d-flex align-items-center gap-2 py-2" href="#">
|
||
<img src="${m.avatar_url || 'assets/images/default-avatar.png'}" style="width: 20px; height: 20px; border-radius: 50%;">
|
||
<span style="color: var(--text-primary); font-size: 0.9em;">${m.username}</span>
|
||
</a>`;
|
||
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 = '<div class="text-center p-3 small" style="color: var(--text-primary);">No overrides configured.</div>';
|
||
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 = `<div style="width: 8px; height: 8px; border-radius: 50%; background-color: ${p.role_color || '#99aab5'}; margin-right: 8px; flex-shrink: 0;"></div>`;
|
||
} else {
|
||
icon = `<img src="${p.member_avatar || 'assets/images/default-avatar.png'}" style="width: 16px; height: 16px; border-radius: 50%; margin-right: 8px; flex-shrink: 0;">`;
|
||
}
|
||
|
||
item.innerHTML = `
|
||
${icon}
|
||
<span class="flex-grow-1 text-truncate">${p.display_name}</span>
|
||
`;
|
||
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 = '<div class="text-center p-3 text-muted small">Loading feeds...</div>';
|
||
try {
|
||
const resp = await fetch(`api_v1_rss.php?channel_id=${channelId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
renderRssFeeds(data.feeds);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function renderRssFeeds(feeds) {
|
||
rssFeedsList.innerHTML = '';
|
||
if (feeds.length === 0) {
|
||
rssFeedsList.innerHTML = '<div class="text-center p-3 text-muted small">No RSS feeds configured.</div>';
|
||
return;
|
||
}
|
||
feeds.forEach(feed => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-1';
|
||
item.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||
<span class="small text-truncate" style="max-width: 80%;">${feed.url}</span>
|
||
<button class="btn btn-sm text-danger delete-rss-btn" data-id="${feed.id}">ร</button>
|
||
</div>
|
||
<div class="small text-muted" style="font-size: 0.7em;">Last fetched: ${feed.last_fetched_at || 'Never'}</div>
|
||
`;
|
||
rssFeedsList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
addRssBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
const url = document.getElementById('new-rss-url').value.trim();
|
||
if (!url) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('action', 'add');
|
||
formData.append('channel_id', channelId);
|
||
formData.append('url', url);
|
||
|
||
const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData });
|
||
if ((await resp.json()).success) {
|
||
document.getElementById('new-rss-url').value = '';
|
||
loadRssFeeds();
|
||
}
|
||
});
|
||
|
||
syncRssBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
syncRssBtn.disabled = true;
|
||
syncRssBtn.textContent = 'Syncing...';
|
||
|
||
const formData = new FormData();
|
||
formData.append('action', 'sync');
|
||
formData.append('channel_id', channelId);
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData });
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
alert(`Sync complete! Found ${result.new_items} new items.`);
|
||
loadRssFeeds();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
|
||
syncRssBtn.disabled = false;
|
||
syncRssBtn.textContent = 'Sync Now';
|
||
});
|
||
|
||
rssFeedsList?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-rss-btn')) {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
const feedId = e.target.dataset.id;
|
||
const formData = new FormData();
|
||
formData.append('action', 'delete');
|
||
formData.append('channel_id', channelId);
|
||
formData.append('feed_id', feedId);
|
||
|
||
await fetch('api_v1_rss.php', { method: 'POST', body: formData });
|
||
loadRssFeeds();
|
||
}
|
||
});
|
||
|
||
// 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 `<img src="${escapeHTML(icon)}" class="role-icon ms-1" style="width: ${size}; height: ${size}; vertical-align: middle; object-fit: contain;">`;
|
||
} else if (icon.startsWith(':') && icon.endsWith(':')) {
|
||
const ce = (window.CUSTOM_EMOTES_CACHE || []).find(e => e.code === icon);
|
||
if (ce) {
|
||
return `<img src="${ce.path}" class="role-icon ms-1" style="width: ${size}; height: ${size}; vertical-align: middle; object-fit: contain;">`;
|
||
}
|
||
return `<span class="ms-1" style="font-size: ${size}; vertical-align: middle;">${escapeHTML(icon)}</span>`;
|
||
} else {
|
||
return `<span class="ms-1" style="font-size: ${size}; vertical-align: middle;">${escapeHTML(icon)}</span>`;
|
||
}
|
||
}
|
||
|
||
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.dataset.badgeData = m.badge_data || '';
|
||
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 = `
|
||
<div class="message-avatar" style="width: 32px; height: 32px; background-color: ${statusColor}; position: relative; ${avatarBg}">
|
||
${m.status === 'online' ? `<div style="position: absolute; bottom: 0; right: 0; width: 10px; height: 10px; background-color: #23a559; border-radius: 50%; border: 2px solid var(--bg-members);"></div>` : ''}
|
||
</div>
|
||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; ${m.role_color ? `color: ${m.role_color};` : ''}">
|
||
${escapeHTML(m.username)}
|
||
${roleIconHtml}
|
||
</span>
|
||
`;
|
||
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 = '<div class="text-center p-3 text-muted">No roles created yet.</div>';
|
||
}
|
||
roles.forEach(role => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-1 rounded role-sortable-item';
|
||
item.dataset.id = role.id;
|
||
const roleIconHtml = renderRoleIconJS(role.icon_url, '14px');
|
||
item.innerHTML = `
|
||
<div class="d-flex align-items-center">
|
||
<div class="role-drag-handle me-3" style="cursor: grab; opacity: 0.5;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="5" x2="8" y2="5.01"></line><line x1="16" y1="5" x2="16" y2="5.01"></line><line x1="8" y1="12" x2="8" y2="12.01"></line><line x1="16" y1="12" x2="16" y2="12.01"></line><line x1="8" y1="19" x2="8" y2="19.01"></line><line x1="16" y1="19" x2="16" y2="19.01"></line></svg>
|
||
</div>
|
||
<div style="width: 14px; height: 14px; border-radius: 50%; background-color: ${role.color}; margin-right: 12px; box-shadow: 0 0 5px ${role.color}88;"></div>
|
||
<span class="fw-medium">${role.name}</span>
|
||
${roleIconHtml}
|
||
</div>
|
||
<div>
|
||
<button class="btn btn-sm btn-outline-light edit-role-btn-v2" data-id="${role.id}" data-name="${role.name}" data-color="${role.color}" data-perms="${role.permissions}" data-icon="${role.icon_url || ''}">Edit</button>
|
||
<button class="btn btn-sm btn-outline-danger delete-role-btn" data-id="${role.id}">ร</button>
|
||
</div>
|
||
`;
|
||
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');
|
||
let badgesHtml = '';
|
||
if (member.badge_data) {
|
||
member.badge_data.split(':::').forEach(d => {
|
||
const parts = d.split('|');
|
||
const name = parts[0];
|
||
const url = parts[1];
|
||
badgesHtml += `<img src="${url}" class="ms-1" style="width: 32px; height: 32px; object-fit: contain;" title="${escapeHTML(name)}">`;
|
||
});
|
||
}
|
||
|
||
item.innerHTML = `
|
||
<div class="d-flex align-items-center flex-grow-1">
|
||
<div class="message-avatar me-2" style="width: 32px; height: 32px; ${member.avatar_url ? `background-image: url('${member.avatar_url}');` : ''}"></div>
|
||
<div class="flex-grow-1">
|
||
<div class="fw-bold small d-flex align-items-center" style="color: ${member.role_color || 'inherit'}">
|
||
${escapeHTML(member.username)}
|
||
${roleIconHtml}
|
||
<div class="ms-2 d-flex gap-1">${badgesHtml}</div>
|
||
</div>
|
||
<div class="text-muted small">
|
||
${member.role_names ? member.role_names.split(',').join(', ') : 'No roles'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
${(window.isServerOwner || window.canManageServer) ? `
|
||
<button class="btn btn-sm btn-outline-light edit-user-roles-settings-btn" data-id="${member.id}" data-username="${member.username}" data-avatar="${member.avatar_url || ''}">Roles</button>
|
||
<button class="btn btn-sm btn-outline-light edit-user-badges-settings-btn" data-id="${member.id}" data-username="${member.username}" data-avatar="${member.avatar_url || ''}">Badges</button>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
membersList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
// Add listener for the button in members list tab
|
||
membersList?.addEventListener('click', (e) => {
|
||
const btnRole = e.target.closest('.edit-user-roles-settings-btn');
|
||
if (btnRole) {
|
||
openEditUserRolesModal(btnRole.dataset.id, btnRole.dataset.username, btnRole.dataset.avatar);
|
||
}
|
||
const btnBadge = e.target.closest('.edit-user-badges-settings-btn');
|
||
if (btnBadge) {
|
||
openEditUserBadgesModal(btnBadge.dataset.id, btnBadge.dataset.username, btnBadge.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 += `
|
||
<div class="form-check mb-1">
|
||
<input class="form-check-input perm-check" type="checkbox" value="${p.value}" id="perm-${p.value}" ${isChecked ? 'checked' : ''}>
|
||
<label class="form-check-label text-white small" for="perm-${p.value}">${p.name}</label>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('roleEditorModal'));
|
||
modal.show();
|
||
}
|
||
});
|
||
|
||
document.getElementById('save-role-btn')?.addEventListener('click', async (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 = '<div class="text-center p-3 text-muted">Loading roles...</div>';
|
||
|
||
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 = `
|
||
<input class="form-check-input me-3 user-role-checkbox" type="checkbox" value="${role.id}" id="user-role-${role.id}" ${isChecked ? 'checked' : ''}>
|
||
<label class="form-check-label flex-grow-1" for="user-role-${role.id}" style="color: ${role.color}; cursor: pointer;">
|
||
${role.name}
|
||
</label>
|
||
`;
|
||
list.appendChild(item);
|
||
});
|
||
|
||
if (data.roles.length === 0) {
|
||
list.innerHTML = '<div class="text-center p-3 text-muted">No roles defined for this server.</div>';
|
||
}
|
||
}
|
||
} 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 += `
|
||
<div class="form-check mb-1">
|
||
<input class="form-check-input perm-check" type="checkbox" value="${p.value}" id="perm-${p.value}">
|
||
<label class="form-check-label text-white small" for="perm-${p.value}">${p.name}</label>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('roleEditorModal'));
|
||
modal.show();
|
||
});
|
||
|
||
rolesList?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-role-btn')) {
|
||
if (!confirm('Delete this role?')) return;
|
||
const roleId = e.target.dataset.id;
|
||
const resp = await fetch('api_v1_roles.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', server_id: activeServerId, id: roleId })
|
||
});
|
||
if ((await resp.json()).success) loadRoles();
|
||
}
|
||
});
|
||
|
||
// Webhooks Management
|
||
const webhooksTabBtn = document.getElementById('webhooks-tab-btn');
|
||
const webhooksList = document.getElementById('webhooks-list');
|
||
const addWebhookBtn = document.getElementById('add-webhook-btn');
|
||
|
||
webhooksTabBtn?.addEventListener('click', loadWebhooks);
|
||
|
||
async function loadWebhooks() {
|
||
webhooksList.innerHTML = '<div class="text-center p-3 text-muted">Loading webhooks...</div>';
|
||
try {
|
||
const resp = await fetch(`api_v1_webhook.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
renderWebhooks(data.webhooks);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function renderWebhooks(webhooks) {
|
||
webhooksList.innerHTML = '';
|
||
if (webhooks.length === 0) {
|
||
webhooksList.innerHTML = '<div class="text-center p-3 text-muted">No webhooks found.</div>';
|
||
return;
|
||
}
|
||
webhooks.forEach(wh => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-2';
|
||
const url = `${window.location.origin}/api_v1_webhook.php?token=${wh.token}`;
|
||
item.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||
<span class="fw-bold">${wh.name}</span>
|
||
<button class="btn btn-sm btn-outline-danger delete-webhook-btn" data-id="${wh.id}">ร</button>
|
||
</div>
|
||
<div class="small text-muted mb-2">Channel: #${wh.channel_name}</div>
|
||
<div class="input-group input-group-sm">
|
||
<input type="text" class="form-control bg-dark text-white border-secondary" value="${url}" readonly>
|
||
<button class="btn btn-outline-secondary" type="button" onclick="navigator.clipboard.writeText('${url}')">Copy</button>
|
||
</div>
|
||
`;
|
||
webhooksList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
addWebhookBtn?.addEventListener('click', async () => {
|
||
const name = prompt('Webhook name:', 'Bot Name');
|
||
if (!name) return;
|
||
|
||
// Fetch channels for this server to let user pick one
|
||
const respChannels = await fetch(`api_v1_channels.php?server_id=${activeServerId}`);
|
||
const dataChannels = await respChannels.json();
|
||
if (!dataChannels.length) return alert('Create a channel first.');
|
||
|
||
const channelId = prompt('Enter Channel ID:\n' + dataChannels.map(c => `${c.id}: #${c.name}`).join('\n'));
|
||
if (!channelId) return;
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_webhook.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ channel_id: channelId, name: name })
|
||
});
|
||
if ((await resp.json()).success) loadWebhooks();
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
webhooksList?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-webhook-btn')) {
|
||
if (!confirm('Delete this webhook?')) return;
|
||
const whId = e.target.dataset.id;
|
||
const resp = await fetch('api_v1_webhook.php', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: whId })
|
||
});
|
||
if ((await resp.json()).success) loadWebhooks();
|
||
}
|
||
});
|
||
|
||
// Stats Management
|
||
const statsTabBtn = document.getElementById('stats-tab-btn');
|
||
statsTabBtn?.addEventListener('click', loadStats);
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const resp = await fetch(`api_v1_stats.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
document.getElementById('stat-members').textContent = data.stats.total_members;
|
||
document.getElementById('stat-messages').textContent = data.stats.total_messages;
|
||
|
||
const topUsersList = document.getElementById('top-users-list');
|
||
topUsersList.innerHTML = '';
|
||
data.stats.top_users.forEach(user => {
|
||
const item = document.createElement('div');
|
||
item.className = 'd-flex justify-content-between align-items-center mb-1 p-2 bg-dark rounded';
|
||
item.innerHTML = `<span>${user.username}</span><span class="badge bg-primary">${user.message_count} msgs</span>`;
|
||
topUsersList.appendChild(item);
|
||
});
|
||
|
||
const activity = document.getElementById('activity-chart-placeholder');
|
||
activity.innerHTML = '';
|
||
data.stats.history.forEach(day => {
|
||
const bar = document.createElement('div');
|
||
bar.className = 'd-flex align-items-center mb-1';
|
||
const percent = Math.min(100, (day.count / 100) * 100); // Normalize to 100 for visual
|
||
bar.innerHTML = `
|
||
<div style="width: 80px;" class="small">${day.date}</div>
|
||
<div class="flex-grow-1 mx-2" style="height: 10px; background: var(--separator); border-radius: 5px;">
|
||
<div style="width: ${percent}%; height: 100%; background: var(--blurple); border-radius: 5px;"></div>
|
||
</div>
|
||
<div style="width: 30px;" class="small text-end">${day.count}</div>
|
||
`;
|
||
activity.appendChild(bar);
|
||
});
|
||
if (data.stats.history.length === 0) {
|
||
activity.innerHTML = '<div class="text-muted">No activity in the last 7 days.</div>';
|
||
}
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
// Server Settings
|
||
const searchServerIconBtn = document.getElementById('search-server-icon-btn');
|
||
const serverIconResults = document.getElementById('server-icon-search-results');
|
||
const serverIconPreview = document.getElementById('server-icon-preview');
|
||
const serverIconUrlInput = document.getElementById('server-icon-url');
|
||
|
||
searchServerIconBtn?.addEventListener('click', async () => {
|
||
const query = prompt('Search for a server icon:', 'abstract');
|
||
if (!query) return;
|
||
|
||
serverIconResults.innerHTML = '<div class="text-muted small">Searching...</div>';
|
||
try {
|
||
const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(query)}`);
|
||
const data = await resp.json();
|
||
serverIconResults.innerHTML = '';
|
||
data.forEach(photo => {
|
||
const img = document.createElement('img');
|
||
img.src = photo.url;
|
||
img.className = 'avatar-pick';
|
||
img.style.width = '50px';
|
||
img.style.height = '50px';
|
||
img.onclick = () => {
|
||
serverIconUrlInput.value = photo.url;
|
||
serverIconPreview.style.backgroundImage = `url('${photo.url}')`;
|
||
serverIconResults.innerHTML = '';
|
||
};
|
||
serverIconResults.appendChild(img);
|
||
});
|
||
} catch (e) {
|
||
serverIconResults.innerHTML = '<div class="text-danger small">Error fetching icons</div>';
|
||
}
|
||
});
|
||
|
||
const badgesTabBtn = document.getElementById('badges-tab-btn');
|
||
const badgesList = document.getElementById('badges-list');
|
||
const addBadgeBtn = document.getElementById('add-badge-btn');
|
||
const saveBadgeBtn = document.getElementById('save-badge-btn');
|
||
const badgeImageUploadInput = document.getElementById('badge-image-upload-input');
|
||
|
||
badgesTabBtn?.addEventListener('click', loadBadges);
|
||
|
||
async function loadBadges() {
|
||
try {
|
||
const resp = await fetch(`api_v1_badges.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
renderBadges(data.badges);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function renderBadges(badges) {
|
||
if (!badgesList) return;
|
||
badgesList.innerHTML = '';
|
||
if (badges.length === 0) {
|
||
badgesList.innerHTML = '<div class="text-center p-3 text-muted">Aucun badge crรฉรฉ.</div>';
|
||
return;
|
||
}
|
||
badges.forEach(badge => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-1 rounded';
|
||
item.innerHTML = `
|
||
<div class="d-flex align-items-center">
|
||
<img src="${badge.image_url}" style="width: 32px; height: 32px; object-fit: contain; margin-right: 12px;">
|
||
<span class="fw-medium">${escapeHTML(badge.name)}</span>
|
||
</div>
|
||
<div>
|
||
<button class="btn btn-sm btn-outline-light edit-badge-btn" data-id="${badge.id}" data-name="${badge.name}" data-url="${badge.image_url}">Modifier</button>
|
||
<button class="btn btn-sm btn-outline-danger delete-badge-btn" data-id="${badge.id}">ร</button>
|
||
</div>
|
||
`;
|
||
badgesList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
addBadgeBtn?.addEventListener('click', () => {
|
||
document.getElementById('edit-badge-id').value = '';
|
||
document.getElementById('edit-badge-name').value = '';
|
||
document.getElementById('edit-badge-image-url').value = '';
|
||
document.getElementById('badge-image-preview').style.backgroundImage = 'none';
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('badgeEditorModal'));
|
||
modal.show();
|
||
});
|
||
|
||
badgesList?.addEventListener('click', (e) => {
|
||
if (e.target.classList.contains('edit-badge-btn')) {
|
||
const b = e.target.dataset;
|
||
document.getElementById('edit-badge-id').value = b.id;
|
||
document.getElementById('edit-badge-name').value = b.name;
|
||
document.getElementById('edit-badge-image-url').value = b.url;
|
||
document.getElementById('badge-image-preview').style.backgroundImage = `url('${b.url}')`;
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('badgeEditorModal'));
|
||
modal.show();
|
||
} else if (e.target.classList.contains('delete-badge-btn')) {
|
||
if (confirm('Supprimer ce badge ?')) {
|
||
const id = e.target.dataset.id;
|
||
fetch('api_v1_badges.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', server_id: activeServerId, id })
|
||
}).then(r => r.json()).then(data => {
|
||
if (data.success) loadBadges();
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
badgeImageUploadInput?.addEventListener('change', async () => {
|
||
const file = badgeImageUploadInput.files[0];
|
||
if (!file) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('badge_image', file);
|
||
formData.append('server_id', activeServerId);
|
||
|
||
try {
|
||
const resp = await fetch('api/upload_badge_image.php', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
document.getElementById('edit-badge-image-url').value = data.url;
|
||
document.getElementById('badge-image-preview').style.backgroundImage = `url('${data.url}')`;
|
||
} else {
|
||
alert(data.error || 'Erreur d\'upload');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
saveBadgeBtn?.addEventListener('click', async () => {
|
||
const id = document.getElementById('edit-badge-id').value;
|
||
const name = document.getElementById('edit-badge-name').value;
|
||
const image_url = document.getElementById('edit-badge-image-url').value;
|
||
|
||
if (!name || !image_url) {
|
||
alert('Le nom et l\'image sont requis.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const action = id ? 'update' : 'create';
|
||
const resp = await fetch('api_v1_badges.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action, server_id: activeServerId, id, name, image_url })
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
bootstrap.Modal.getInstance(document.getElementById('badgeEditorModal')).hide();
|
||
loadBadges();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// User Badges Assignment Logic
|
||
async function openEditUserBadgesModal(userId, username, avatar) {
|
||
document.getElementById('edit-user-badges-user-id').value = userId;
|
||
document.getElementById('edit-user-badges-username').textContent = username;
|
||
document.getElementById('edit-user-badges-avatar').style.backgroundImage = avatar ? `url('${avatar}')` : 'none';
|
||
|
||
const list = document.getElementById('user-badges-selection-list');
|
||
list.innerHTML = '<div class="text-center p-3 text-muted">Chargement...</div>';
|
||
|
||
const bsModal = new bootstrap.Modal(document.getElementById('editUserBadgesModal'));
|
||
bsModal.show();
|
||
|
||
try {
|
||
const [badgeResp, memberBadgeResp] = await Promise.all([
|
||
fetch(`api_v1_badges.php?server_id=${activeServerId}`),
|
||
fetch(`api_v1_roles.php?server_id=${activeServerId}`) // We need member info
|
||
]);
|
||
|
||
const badgeData = await badgeResp.json();
|
||
const memberData = await memberBadgeResp.json();
|
||
|
||
if (badgeData.success && memberData.success) {
|
||
const member = memberData.members.find(m => m.id == userId);
|
||
// We'll use a hack to get user badges if not provided by roles API yet,
|
||
// but actually I updated api_v1_roles.php to include badge_urls.
|
||
// Wait, badge_urls are URLs, but I need IDs for the checkboxes.
|
||
|
||
// Let's refine api_v1_roles.php to also return badge_ids.
|
||
// Or I can fetch it specifically if needed.
|
||
// For now, I'll fetch it from a dedicated endpoint if I add it,
|
||
// but let's just update api_v1_roles.php one more time.
|
||
|
||
const assignedBadgeUrls = member && member.badge_data ? member.badge_data.split(':::').map(d => d.split('|')[1]) : [];
|
||
|
||
list.innerHTML = '';
|
||
badgeData.badges.forEach(badge => {
|
||
const isChecked = assignedBadgeUrls.includes(badge.image_url);
|
||
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 = `
|
||
<input class="form-check-input me-3 user-badge-checkbox" type="checkbox" value="${badge.id}" id="user-badge-${badge.id}" ${isChecked ? 'checked' : ''}>
|
||
<label class="form-check-label flex-grow-1 d-flex align-items-center" for="user-badge-${badge.id}" style="cursor: pointer;">
|
||
<img src="${badge.image_url}" style="width: 32px; height: 32px; object-fit: contain; margin-right: 8px;">
|
||
${escapeHTML(badge.name)}
|
||
</label>
|
||
`;
|
||
list.appendChild(item);
|
||
});
|
||
|
||
if (badgeData.badges.length === 0) {
|
||
list.innerHTML = '<div class="text-center p-3 text-muted">Aucun badge dรฉfini pour ce serveur.</div>';
|
||
}
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
document.getElementById('save-user-badges-btn')?.addEventListener('click', async () => {
|
||
const btn = document.getElementById('save-user-badges-btn');
|
||
const userId = document.getElementById('edit-user-badges-user-id').value;
|
||
const badgeIds = Array.from(document.querySelectorAll('.user-badge-checkbox:checked')).map(cb => cb.value);
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_badges.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
action: 'set_user_badges',
|
||
server_id: activeServerId,
|
||
user_id: userId,
|
||
badge_ids: badgeIds
|
||
})
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
bootstrap.Modal.getInstance(document.getElementById('editUserBadgesModal')).hide();
|
||
loadRoles(); // Refresh members list
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Forum: New Thread
|
||
const newThreadBtn = document.getElementById('new-thread-btn');
|
||
const newThreadModal = document.getElementById('newThreadModal') ? new bootstrap.Modal(document.getElementById('newThreadModal')) : null;
|
||
let selectedTagIds = [];
|
||
|
||
newThreadBtn?.addEventListener('click', async () => {
|
||
if (!newThreadModal) return;
|
||
|
||
// Load tags for this channel
|
||
const tagsList = document.getElementById('new-thread-tags-list');
|
||
tagsList.innerHTML = '<div class="text-muted small">Loading tags...</div>';
|
||
selectedTagIds = [];
|
||
|
||
try {
|
||
const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`);
|
||
const data = await resp.json();
|
||
tagsList.innerHTML = '';
|
||
if (data.success && data.tags.length > 0) {
|
||
data.tags.forEach(tag => {
|
||
const span = document.createElement('span');
|
||
span.className = 'badge rounded-pill p-2 border border-secondary';
|
||
span.style.cursor = 'pointer';
|
||
span.style.backgroundColor = 'transparent';
|
||
span.dataset.id = tag.id;
|
||
span.dataset.color = tag.color;
|
||
span.textContent = tag.name;
|
||
span.onclick = () => {
|
||
if (selectedTagIds.includes(tag.id)) {
|
||
selectedTagIds = selectedTagIds.filter(id => id !== tag.id);
|
||
span.style.backgroundColor = 'transparent';
|
||
} else {
|
||
selectedTagIds.push(tag.id);
|
||
span.style.backgroundColor = tag.color;
|
||
}
|
||
};
|
||
tagsList.appendChild(span);
|
||
});
|
||
} else {
|
||
tagsList.innerHTML = '<div class="text-muted small">No tags available.</div>';
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
|
||
newThreadModal.show();
|
||
});
|
||
|
||
document.getElementById('submit-new-thread-btn')?.addEventListener('click', async () => {
|
||
const title = document.getElementById('new-thread-title').value.trim();
|
||
if (!title) return;
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('channel_id', currentChannel);
|
||
formData.append('title', title);
|
||
formData.append('tag_ids', selectedTagIds.join(','));
|
||
const resp = await fetch('api_v1_threads.php', { method: 'POST', body: formData });
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
window.location.href = `?server_id=${activeServerId}&channel_id=${currentChannel}&thread_id=${result.thread_id}`;
|
||
} else {
|
||
alert(result.error || 'Failed to create thread');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Forum: Mark as Solution
|
||
document.addEventListener('click', async (e) => {
|
||
const solBtn = e.target.closest('.action-btn.mark-solution');
|
||
if (solBtn) {
|
||
const threadId = solBtn.dataset.threadId;
|
||
const messageId = solBtn.classList.contains('active') ? null : solBtn.dataset.messageId;
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_threads.php?action=solve', {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ thread_id: threadId, message_id: messageId })
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(result.error || 'Failed to update solution');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
});
|
||
|
||
// Forum: Manage Tags
|
||
const manageTagsBtn = document.getElementById('manage-tags-btn');
|
||
const manageTagsModal = document.getElementById('manageTagsModal') ? new bootstrap.Modal(document.getElementById('manageTagsModal')) : null;
|
||
|
||
manageTagsBtn?.addEventListener('click', async () => {
|
||
if (!manageTagsModal) return;
|
||
loadForumAdminTags();
|
||
manageTagsModal.show();
|
||
});
|
||
|
||
// 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 = '<div class="text-center p-3 text-muted small">Loading tags...</div>';
|
||
try {
|
||
const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`);
|
||
const data = await resp.json();
|
||
list.innerHTML = '';
|
||
if (data.success && data.tags.length > 0) {
|
||
data.tags.forEach(tag => {
|
||
const div = document.createElement('div');
|
||
div.className = 'd-flex justify-content-between align-items-center mb-2 p-2 bg-dark rounded';
|
||
div.innerHTML = `
|
||
<div class="d-flex align-items-center">
|
||
<div style="width: 14px; height: 14px; border-radius: 50%; background-color: ${tag.color}; margin-right: 8px;"></div>
|
||
<span>${tag.name}</span>
|
||
</div>
|
||
<button class="btn btn-sm text-danger delete-forum-tag-btn" data-id="${tag.id}">ร</button>
|
||
`;
|
||
list.appendChild(div);
|
||
});
|
||
} else {
|
||
list.innerHTML = '<div class="text-center p-3 text-muted small">No tags created yet.</div>';
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
document.getElementById('add-forum-tag-btn')?.addEventListener('click', async () => {
|
||
const name = document.getElementById('new-tag-name').value.trim();
|
||
const color = document.getElementById('new-tag-color').value;
|
||
if (!name) return;
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_tags.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'create', channel_id: currentChannel, name, color })
|
||
});
|
||
if ((await resp.json()).success) {
|
||
document.getElementById('new-tag-name').value = '';
|
||
loadForumAdminTags();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
document.getElementById('forum-tags-admin-list')?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-forum-tag-btn')) {
|
||
const tagId = e.target.dataset.id;
|
||
try {
|
||
const resp = await fetch('api_v1_tags.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', channel_id: currentChannel, tag_id: tagId })
|
||
});
|
||
if ((await resp.json()).success) loadForumAdminTags();
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
});
|
||
|
||
// Rules: Add Rule
|
||
const addRuleBtn = document.getElementById('add-rule-btn');
|
||
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 = '<span class="spinner-border spinner-border-sm me-2"></span> 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 = '<div class="alert alert-success d-inline-block"><i class="fa-solid fa-check-circle me-2"></i> Vous avez acceptรฉ les rรจgles.</div>';
|
||
// 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 = '<i class="fa-solid fa-check me-2"></i> J\'accepte les rรจgles';
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="fa-solid fa-check me-2"></i> 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 = '<span class="spinner-border spinner-border-sm me-2"></span> 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 = '<i class="fa-solid fa-undo me-1"></i> Retirer mon acceptation';
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="fa-solid fa-undo me-1"></i> 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 = '<div class="spinner-border spinner-border-sm text-light" role="status"></div>';
|
||
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 = '<div class="spinner-border spinner-border-sm text-light" role="status"></div>';
|
||
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 = '<div class="text-muted small">Searching...</div>';
|
||
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 = '<div class="text-muted small">Aucun rรฉsultat trouvรฉ.</div>';
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
avatarResults.innerHTML = '<div class="text-danger small">Erreur lors de la rรฉcupรฉration.</div>';
|
||
}
|
||
}
|
||
|
||
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(`<pre class="code-block"><code class="language-${lang || 'text'}">${content}</code></pre>`);
|
||
return placeholder;
|
||
});
|
||
|
||
// Inline code: `content`
|
||
const inlineCodes = [];
|
||
html = html.replace(/`([^`\n]+)`/g, (match, content) => {
|
||
const placeholder = `__INLINE_CODE_${inlineCodes.length}__`;
|
||
inlineCodes.push(`<code>${content}</code>`);
|
||
return placeholder;
|
||
});
|
||
|
||
// Bold: **text**
|
||
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||
|
||
// Italics: *text* or _text_
|
||
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||
html = html.replace(/_([^_]+)_/g, '<em>$1</em>');
|
||
|
||
// Underline: __text__
|
||
html = html.replace(/__([^_]+)__/g, '<u>$1</u>');
|
||
|
||
// Strikethrough: ~~text~~
|
||
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||
|
||
// Spoiler: ||text||
|
||
html = html.replace(/\|\|([^|]+)\|\|/g, '<span class="spoiler" onclick="this.classList.toggle(\'revealed\')">$1</span>');
|
||
|
||
// Headers: # H1, ## H2, ### H3 (must be at start of line)
|
||
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
|
||
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
|
||
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
|
||
|
||
// Subtext: -# text (must be at start of line)
|
||
html = html.replace(/^-# (.*$)/gm, '<small class="text-muted d-block" style="font-size: 0.8em;">$1</small>');
|
||
|
||
// Blockquotes: > text or >>> text (must be at start of line)
|
||
html = html.replace(/^> (.*$)/gm, '<blockquote>$1</blockquote>');
|
||
html = html.replace(/^>>> ([\s\S]*$)/g, '<blockquote>$1</blockquote>');
|
||
|
||
// Hyperlinks: [text](url)
|
||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||
|
||
// Pure links: <url>
|
||
html = html.replace(/<(https?:\/\/[^&]+)>/g, '<a href="$1" target="_blank">$1</a>');
|
||
|
||
// Newlines to <br> (only those not inside placeholders)
|
||
html = html.replace(/\n/g, '<br>');
|
||
|
||
// Remove extra space around headers and blockquotes added by nl2br
|
||
html = html.replace(/(<br>)\s*(<h1>|<h2>|<h3>|<blockquote>)/gi, '$2');
|
||
html = html.replace(/(<\/h1>|<\/h2>|<\/h3>|<\/blockquote>)\s*(<br>)/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 = `<img src="${emote.path}" alt="${emote.name}" title="${emote.code}" style="width: 24px; height: 24px; vertical-align: middle; object-fit: contain;">`;
|
||
// 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 = `<div class="message-attachment mt-2"><img src="${msg.attachment_url}" class="img-fluid rounded message-img-preview" alt="Attachment" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)"></div>`;
|
||
} else {
|
||
attachmentHtml = `<div class="message-attachment mt-2"><a href="${msg.attachment_url}" target="_blank" class="attachment-link d-inline-flex align-items-center p-2 rounded bg-dark text-white text-decoration-none"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>${msg.attachment_url.split('/').pop()}</a></div>`;
|
||
}
|
||
}
|
||
|
||
let embedHtml = '';
|
||
if (msg.metadata) {
|
||
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata;
|
||
embedHtml = `
|
||
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid var(--blurple); max-width: 520px;">
|
||
${meta.site_name ? `<div class="embed-site-name mb-1" style="font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; font-weight: bold;">${escapeHTML(meta.site_name)}</div>` : ''}
|
||
${meta.title ? `<a href="${meta.url}" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc;">${escapeHTML(meta.title)}</a>` : ''}
|
||
${meta.description ? `<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);">${escapeHTML(meta.description)}</div>` : ''}
|
||
${meta.image ? `<div class="embed-image"><img src="${meta.image}" class="rounded" style="max-width: 100%; max-height: 300px; object-fit: contain;"></div>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const isMe = msg.user_id == window.currentUserId || msg.username == window.currentUsername;
|
||
const hasManageRights = window.canManageChannels || window.isServerOwner || false;
|
||
|
||
const pinHtml = `
|
||
<span class="action-btn pin ${msg.is_pinned ? 'active' : ''}" title="${msg.is_pinned ? 'Unpin' : 'Pin'}" data-id="${msg.id}" data-pinned="${msg.is_pinned ? '1' : '0'}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
|
||
</span>
|
||
`;
|
||
|
||
const actionsHtml = (isMe || hasManageRights) ? `
|
||
<div class="message-actions-menu">
|
||
${pinHtml}
|
||
${isMe ? `
|
||
<span class="action-btn edit" title="Edit" data-id="${msg.id}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
|
||
</span>
|
||
<span class="action-btn delete" title="Delete" data-id="${msg.id}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
||
</span>
|
||
` : ''}
|
||
</div>
|
||
` : '';
|
||
|
||
const pinnedBadge = msg.is_pinned ? `
|
||
<span class="pinned-badge ms-2" title="Pinned Message">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path></svg>
|
||
Pinned
|
||
</span>
|
||
` : '';
|
||
|
||
let userBadgesHtml = '';
|
||
const bData = msg.badge_data || '';
|
||
if (bData) {
|
||
bData.split(':::').forEach(d => {
|
||
const parts = d.split('|');
|
||
const name = parts[0];
|
||
const url = parts[1];
|
||
userBadgesHtml += `<img src="${url}" class="ms-1" style="width: 32px; height: 32px; object-fit: contain; vertical-align: middle;" title="${escapeHTML(name)}">`;
|
||
});
|
||
}
|
||
|
||
const mentionRegex = new RegExp(`@${window.currentUsername}\\b`, 'g');
|
||
const mentionHtml = `<span class="mention">@${window.currentUsername}</span>`;
|
||
const contentWithMentions = parseCustomEmotes(msg.content).replace(mentionRegex, mentionHtml);
|
||
|
||
div.innerHTML = `
|
||
<div class="message-avatar" style="${avatarStyle}"></div>
|
||
<div class="message-content">
|
||
<div class="message-header">
|
||
<span class="message-username" style="color: ${msg.role_color || 'inherit'};">
|
||
${escapeHTML(msg.username)}
|
||
${renderRoleIconJS(msg.role_icon, '14px')}
|
||
<span class="ms-1 d-inline-flex gap-1">${userBadgesHtml}</span>
|
||
</span>
|
||
<span class="message-timestamp">${msg.timestamp || 'Just now'}</span>
|
||
${pinnedBadge}
|
||
</div>
|
||
<div class="message-text">${contentWithMentions}</div>
|
||
${attachmentHtml}
|
||
${embedHtml}
|
||
<div class="message-reactions mt-1" data-message-id="${msg.id}"></div>
|
||
</div>
|
||
${actionsHtml}
|
||
<div class="message-reaction-picker-anchor"></div>
|
||
`;
|
||
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: <span id="invite-timer-display">30:00</span>';
|
||
}
|
||
} 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 = '<span class="text-danger">Expired</span>';
|
||
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();
|
||
}
|
||
});
|