1983 lines
100 KiB
JavaScript
1983 lines
100 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);
|
||
}
|
||
}
|
||
|
||
// Emoji list categorized
|
||
const EMOJI_CATEGORIES = {
|
||
'Smileys': ['๐', '๐', '๐', '๐', '๐', '๐
', '๐คฃ', '๐', '๐', '๐', '๐', '๐', '๐', '๐ฅฐ', '๐', '๐คฉ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐คช', '๐', '๐ค', '๐ค', '๐คญ', '๐คซ', '๐ค', '๐ค', '๐คจ', '๐', '๐', '๐ถ', '๐', '๐', '๐', '๐ฌ', '๐คฅ', '๐', '๐', '๐ช', '๐คค', '๐ด', '๐ท', '๐ค', '๐ค', '๐คข', '๐คฎ', '๐คง', '๐ฅต', '๐ฅถ', '๐ฅด', '๐ต', '๐คฏ', '๐ค ', '๐ฅณ', '๐', '๐ค', '๐ง', '๐', '๐', '๐', 'โน๏ธ', '๐ฎ', '๐ฏ', '๐ฒ', '๐ณ', '๐ฅบ', '๐ฆ', '๐ง', '๐จ', '๐ฐ', '๐ฅ', '๐ข', '๐ญ', '๐ฑ', '๐', '๐ฃ', '๐', '๐', '๐ฉ', '๐ซ', '๐ฅฑ', '๐ค', '๐ก', '๐ ', '๐คฌ', '๐', '๐ฟ', '๐น', '๐บ', '๐', 'โ ๏ธ', '๐ฉ', '๐คก', '๐น', '๐บ', '๐ป', '๐ฝ', '๐พ', '๐ค', '๐บ', '๐ธ', '๐ป', '๐ผ', '๐ฝ', '๐', '๐ฟ', '๐พ'],
|
||
'People': ['๐', '๐ค', '๐๏ธ', 'โ', '๐', '๐', '๐ค', 'โ๏ธ', '๐ค', '๐ค', '๐ค', '๐ค', '๐', '๐', '๐', '๐', '๐', 'โ๏ธ', '๐', '๐', 'โ', '๐', '๐ค', '๐ค', '๐', '๐', '๐', '๐คฒ', '๐ค', '๐', 'โ๏ธ', '๐
', '๐คณ', '๐ช', '๐ฆพ', '๐ฆต', '๐ฆฟ', '๐ฆถ', '๐', '๐ฆป', '๐', '๐ง ', '๐ฆท', '๐ฆด', '๐', '๐๏ธ', '๐
', '๐', '๐ถ', '๐ง', '๐ฆ', '๐ง', '๐ง', '๐ฑ', '๐จ', '๐ง', '๐ฉ', '๐ง', '๐ด', '๐ต', '๐ฎ', '๐ต๏ธ', '๐', '๐ท', '๐คด', '๐ธ', '๐ณ', '๐ฒ', '๐ง', '๐คต', '๐ฐ', '๐คฐ', '๐คฑ', '๐ผ', '๐
', '๐คถ', '๐ฆธ', '๐ฆน', '๐ง', '๐ง', '๐ง', '๐ง', '๐ง', '๐ง', '๐ง', '๐', '๐', '๐ถ', '๐', '๐', '๐บ', '๐ด๏ธ', '๐ฏ', '๐ง', '๐ง'],
|
||
'Animals': ['๐ถ', '๐ฑ', '๐ญ', '๐น', '๐ฐ', '๐ฆ', '๐ป', '๐ผ', '๐จ', '๐ฏ', '๐ฆ', '๐ฎ', '๐ท', '๐ฝ', '๐ธ', '๐ต', '๐', '๐', '๐', '๐', '๐', '๐ง', '๐ฆ', '๐ค', '๐ฃ', '๐ฅ', '๐ฆ', '๐ฆ
', '๐ฆ', '๐ฆ', '๐บ', '๐', '๐ด', '๐ฆ', '๐', '๐', '๐ฆ', '๐', '๐', '๐', '๐ฆ', '๐ฆ', '๐ท๏ธ', '๐ธ๏ธ', '่ ', '๐ข', '๐', '๐ฆ', '๐ฆ', '๐ฆ', '๐', '๐ฆ', '๐ฆ', '๐ฆ', '๐ฆ', '๐ก', '๐ ', '๐', '๐ฌ', '๐ณ', '๐', '๐ฆ', '๐', '๐
', '๐', '๐ฆ', '๐ฆ', '๐ฆง', '๐', '๐ฆ', '๐ฆ', '๐ช', '๐ซ', '๐ฆ', '๐ฆ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐ฆ', '๐', '๐ฉ', '๐ฆฎ', '๐', '๐', '๐ฆ', '๐ฆ', '๐ฆ', '๐ฆข', '๐ฆฉ', '๐๏ธ', '๐', '๐ฆ', '๐ฆจ', '๐ฆก', '๐ฆฆ', '๐ฆฅ', '๐', '๐', '๐ฟ๏ธ', '๐ฆ'],
|
||
'Nature': ['๐ต', '๐', '๐ฒ', '๐ณ', '๐ด', '๐ฑ', '๐ฟ', 'โ๏ธ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐พ', '๐', '๐ท', '๐น', '๐ฅ', '๐บ', '๐ธ', '๐ผ', '๐ป', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐ช', '๐ซ', 'โญ๏ธ', '๐', 'โจ', 'โก๏ธ', 'โ๏ธ', '๐ฅ', '๐ฅ', '๐ช๏ธ', '๐', 'โ๏ธ', '๐ค๏ธ', 'โ
๏ธ', '๐ฅ๏ธ', 'โ๏ธ', '๐ฆ๏ธ', '๐ง๏ธ', '๐จ๏ธ', '๐ฉ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', '๐ฌ๏ธ', '๐จ', '๐ง', '๐ฆ', 'โ๏ธ', 'โ๏ธ', '๐', '๐ซ๏ธ'],
|
||
'Food': ['๐', '๐', '๐', '๐', '๐', ' BANANA', '๐', '๐', '๐', '๐', '๐', '๐', '๐ฅญ', '๐', '๐ฅฅ', '๐ฅ', '๐
', '๐', '๐ฅ', '๐ฅฆ', '๐ฅฌ', '๐ฅ', '๐ฝ', '๐ฅ', '๐ง', '๐ฅ', '๐ ', '๐ฅ', '๐ฅฏ', '๐', '๐ฅ', '๐ฅจ', '๐ง', '๐ฅ', '๐ณ', '๐ง', '๐ฅ', ' waffle', '๐ฅ', '๐ฅฉ', '๐', '๐', '๐ฆด', '๐ญ', '๐', '๐', '๐', '๐ฅช', '๐ฅ', '๐ง', '๐ฎ', '๐ฏ', '๐ฅ', '๐ฅ', '๐', '๐', '๐ฒ', '๐', ' sushi', ' Bento', ' Dumpling', ' Oyster', '๐ค', ' Rice Ball', ' Rice', ' Rice Cracker', '๐ฅ', '๐ฅ ', '๐ฅฎ', '๐ข', '๐ก', '๐ง', '๐จ', '๐ฆ', '๐ฅง', '๐ง', '๐ฐ', '๐', '๐ฎ', '๐ญ', '๐ฌ', '๐ซ', ' popcorn', '๐ฉ', '๐ช', '๐ฐ', ' peanuts', '๐ฏ', '๐ฅ', 'โ๏ธ', '๐ต', '๐ฅค', '๐ถ', '๐บ', '๐ป', '๐ฅ', '๐ท', '๐ฅ', '๐ธ', '๐น', '๐ง', '๐พ', '๐ง', '๐ฅ', '๐ด', '๐ฝ๏ธ', '๐ฅฃ', '๐ฅก'],
|
||
'Travel': ['๐', '๐', '๐', '๐', '๐', '๐๏ธ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐ต', '๐ฒ', '๐ด', '๐', '๐ฃ๏ธ', '๐ค๏ธ', 'โฝ๏ธ', '๐จ', '๐ฅ', '๐ฆ', '๐ง', 'โ๏ธ', 'โต๏ธ', '๐ค', '๐ณ๏ธ', 'โด๏ธ', '๐ข', 'โ๏ธ', '๐ซ', '๐ฌ', '๐บ', '๐', '๐', '๐ ', '๐ก', '๐', '๐ธ', '๐ฐ๏ธ', 'โ๏ธ', 'โณ', 'โ๏ธ', 'โฐ', 'โฑ๏ธ', 'โฒ๏ธ', '๐ฐ๏ธ', '๐ก๏ธ', 'โ๏ธ', '๐ช', '๐', 'โ๏ธ', 'โ
๏ธ', 'โ๏ธ', '๐', 'โฐ๏ธ', '๐๏ธ', '๐ป', '๐', '๐๏ธ', '๐๏ธ', 'โบ๏ธ', '๐ ', '๐ก', '๐ข', '๐ฃ', '๐ค', '๐ฅ', '๐ฆ', '๐จ', '๐ฉ', '๐ช', '๐ซ', '๐ฌ', '๐ญ', '๐ฏ', '๐ฐ', '๐', '๐ผ', '๐ฝ', 'โช๏ธ', '๐', '๐', 'โฉ๏ธ', '๐', 'โฒ๏ธ', '๐', '๐', '๐๏ธ', '๐', '๐
', '๐', '๐', '๐', '๐ ', '๐ก', '๐ข', '๐', '๐', '๐', '๐
', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐'],
|
||
'Activities': ['โฝ๏ธ', '๐', '๐', 'โพ๏ธ', '๐ฅ', '๐พ', '๐', '๐', '๐ฑ', '๐', '๐ธ', '๐ฅ
', ' hockey', ' field hockey', ' cricket', 'โณ๏ธ', '๐น', '๐ฃ', ' boxing', '๐ฅ', ' skateboard', '๐ท', 'โธ๏ธ', '๐ฅ', '๐ฟ', 'โท๏ธ', '๐', '๐๏ธ', ' fencing', '๐คผ', ' gymnastics', ' basketball player', '๐คฝ', ' handball', ' juggle', '๐ง', '๐', ' rowing', ' swimming', '๐ด', '๐ต', '๐ง', '๐๏ธ', '๐', '๐
', '๐ฅ', '๐ฅ', '๐ฅ', '๐ซ', '๐๏ธ', '๐ญ', '๐จ', '๐ฌ', '๐ค', '๐ง', '๐ผ', '๐น', '๐ฅ', '๐ท', '๐บ', '๐ธ', '๐ช', '๐ป', '๐ฒ', 'โ๏ธ', '๐ฏ', '๐ณ', '๐ฎ', '๐ฐ', '๐งฉ'],
|
||
'Objects': ['โ๏ธ', '๐ฑ', '๐ฒ', '๐ป', 'โจ๏ธ', '๐ฑ๏ธ', '๐ฒ๏ธ', '๐น๏ธ', '๐๏ธ', '๐ฝ', '๐พ', '๐ฟ', '๐๏ธ', '๐ท', '๐ธ', '๐น', '๐ผ', '๐', '๐', '๐ฏ๏ธ', '๐ก', '๐ฆ', '๐ฎ', '๐ช', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐ฐ', '๐๏ธ', '๐', '๐', '๐ท๏ธ', '๐ฐ', '๐ด', '๐ต', '๐ถ', '๐ท', '๐ธ', '๐ณ', '๐งพ', '๐น', 'โ๏ธ', '๐ง', '๐จ', '๐ฉ', '๐ค', '๐ฅ', '๐ฆ', '๐ซ', '๐ฉ', '๐ฌ', '๐ญ', '๐ฎ', '๐ณ๏ธ', 'โ๏ธ', 'โ๏ธ', '๐๏ธ', '๐๏ธ', '๐๏ธ', '๐๏ธ', '๐', '๐ผ', '๐', '๐', '๐๏ธ', '๐
', '๐', '๐๏ธ', '๐๏ธ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐๏ธ', '๐', '๐', 'โ๏ธ', '๐๏ธ', '๐๏ธ', '๐๏ธ', '๐', '๐', '๐', '๐', '๐', '๐๏ธ', '๐จ', '๐ช', 'โ๏ธ', 'โ๏ธ', '๐ ๏ธ', '๐ก๏ธ', 'โ๏ธ', '๐ซ', '๐ก๏ธ', '๐ง', '๐ฉ', 'โ๏ธ', '๐๏ธ', 'โ๏ธ', '๐ฆฏ', '๐', 'โ๏ธ', '๐งฐ', '๐งฒ', 'โ๏ธ', '๐งช', '๐งซ', '๐งฌ', '๐ฌ', '๐ญ', '๐ก', '๐', '๐ฉธ', '๐', '๐ฉน', '๐ฉบ', '๐ช', '๐๏ธ', '๐๏ธ', '๐ช', '๐ฝ', '๐ฟ', '๐', '๐ช', '๐งด', '๐งท', '๐งน', '๐งบ', '๐งป', '๐งผ', '๐งฏ', '๐', '๐ฌ', 'โฐ๏ธ', 'โฑ๏ธ', '๐ฟ'],
|
||
'Symbols': ['๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', 'โฃ๏ธ', '๐', 'โค๏ธ', '๐งก', '๐', '๐', '๐', '๐', '๐ค', '๐ค', '๐ค', '๐ฏ', '๐ข', '๐ฅ', '๐ซ', '๐ฆ', '๐จ', '๐ณ๏ธ', '๐ฃ', '๐ฌ', '๐๏ธโ๐จ๏ธ', '๐จ๏ธ', '๐ฏ๏ธ', '๐ญ', '๐ค', '๐', 'โ ๏ธ', 'โฅ๏ธ', 'โฆ๏ธ', 'โฃ๏ธ', '๐', '๐๏ธ', '๐ด', '๐ญ', '๐', '๐', '๐', '๐', '๐ข', '๐ฃ', '๐ฏ', '๐', '๐', '๐ผ', '๐ต', '๐ถ', '๐น', '๐ง', '๐ฎ', '๐ฐ', 'โฟ๏ธ', '๐น', '๐บ', '๐ป', '๐ผ', '๐พ', '๐', '๐', '๐', '๐
', 'โ ๏ธ', '๐ธ', 'โ๏ธ', '๐ซ', '๐ณ', '๐ญ', '๐ฏ', '๐ฑ', '๐ท', '๐ต', '๐', 'โข๏ธ', 'โฃ๏ธ', 'โฌ๏ธ', 'โ๏ธ', 'โก๏ธ', 'โ๏ธ', 'โฌ๏ธ', 'โ๏ธ', 'โฌ
๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โฉ๏ธ', 'โช๏ธ', 'โคด๏ธ', 'โคต๏ธ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', 'โ๏ธ', '๐๏ธ', 'โก๏ธ', 'โธ๏ธ', 'โฏ๏ธ', 'โ๏ธ', 'โฆ๏ธ', 'โช๏ธ', 'โฎ๏ธ', '๐', '๐ฏ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ', '๐', '๐', '๐', 'โถ๏ธ', 'โฉ', 'โญ๏ธ', 'โฏ๏ธ', 'โ๏ธ', 'โช', 'โฎ๏ธ', '๐ผ', 'โซ', '๐ฝ', 'โฌ', 'โธ๏ธ', 'โน๏ธ', 'โบ๏ธ', 'โ๏ธ', '๐ฆ', '๐
', '๐', '๐ถ', '๐ณ', '๐ด', 'โ', 'โ', 'โ', 'โ๏ธ', 'โพ๏ธ', 'โผ๏ธ', 'โ๏ธ', 'โ', 'โ', 'โ', 'โ๏ธ', 'ใฐ๏ธ', '๐ฑ', '๐ฒ', 'โ๏ธ', 'โป๏ธ', 'โ๏ธ', '๐ฑ', '๐', '๐ฐ', 'โญ๏ธ', 'โ
', 'โ๏ธ', 'โ๏ธ', 'โ๏ธ', 'โ', 'โ', 'โฐ', 'โฟ', 'ใฝ๏ธ', 'โณ๏ธ', 'โด๏ธ', 'โ๏ธ', 'โผ๏ธ', '๐', '๐๏ธ', '๐ท๏ธ', '๐ถ', '๐ฏ๏ธ', '๐', '๐น', '๐๏ธ', '๐ฒ', '๐', '๐ธ', '๐ด', '๐ณ', 'ใ๏ธ', 'ใ๏ธ', '๐บ', '๐ต', '๐ด', '๐ ', '๐ก', '๐ข', '๐ต', '๐ฃ', '๐ค', 'โซ๏ธ', 'โช๏ธ', '๐ฅ', '๐ง', '๐จ', '๐ฉ', '๐ฆ', '๐ช', '๐ซ', 'โฌ๏ธ', 'โฌ๏ธ'],
|
||
'Flags': ['๐', '๐ฉ', '๐', '๐ด', '๐ณ๏ธ', '๐ณ๏ธโ๐', '๐ณ๏ธโโง๏ธ', '๐ดโโ ๏ธ', '๐ฆ๐ซ', '๐ฆ๐ฝ', '๐ฆ๐ฑ', '๐ฉ๐ฟ', '๐ฆ๐ฒ', '๐ฆ๐บ', '๐ฆ๐น', '๐ฆ๐ฟ', '๐ง๐ช', '๐ง๐ท', '๐จ๐ฆ', '๐จ๐ฑ', '๐จ๐ณ', '๐จ๐ด', '๐จ๐ฟ', '๐ฉ๐ฐ', '๐ช๐ฌ', '๐ซ๐ฎ', '๐ซ๐ท', '๐ฉ๐ช', '๐ฌ๐ท', '๐ญ๐ฐ', '๐ฎ๐ณ', '๐ฎ๐ฉ', '๐ฎ๐ช', '๐ฎ๐ฑ', '๐ฎ๐น', '๐ฏ๐ต', '๐ฐ๐ท', '๐ฒ๐ฝ', '๐ณ๐ฑ', '๐ณ๐ฟ', '๐ณ๐ด', '๐ต๐ฐ', '๐ต๐ญ', '๐ต๐ฑ', '๐ต๐น', '๐ท๐บ', '๐ธ๐ฆ', '๐ธ๐ฌ', '๐ฟ๐ฆ', '๐ช๐ธ', '๐ธ๐ช', '๐จ๐ญ', '๐น๐ญ', '๐น๐ท', '๐บ๐ฆ', '๐ฆ๐ช', '๐ฌ๐ง', '๐บ๐ธ', '๐ป๐ณ']
|
||
};
|
||
|
||
// Global EMOJIS list for reactions (flattened)
|
||
const EMOJIS = Object.values(EMOJI_CATEGORIES).flat();
|
||
|
||
function populateEmojiGrid(category = null, searchTerm = '') {
|
||
const roleEmojiGrid = document.getElementById('role-emoji-grid');
|
||
if (!roleEmojiGrid) return;
|
||
|
||
roleEmojiGrid.innerHTML = '';
|
||
let emojis = [];
|
||
|
||
if (searchTerm) {
|
||
emojis = EMOJIS.filter(e => e.includes(searchTerm) || searchTerm === '');
|
||
// For simple search by name, we'd need names, but since we only have the emoji characters,
|
||
// the user might search by the emoji itself or we could just show all if search is empty.
|
||
// Actually, without a mapping of emoji to names, searching is limited.
|
||
// But I'll implement it anyway so if they paste an emoji it works,
|
||
// or I can add common names if I had a mapping.
|
||
// Given the constraint, I'll just filter by character for now,
|
||
// or maybe the user just wants a way to filter the massive list.
|
||
} else {
|
||
emojis = EMOJI_CATEGORIES[category] || [];
|
||
}
|
||
|
||
emojis.forEach(emoji => {
|
||
const span = document.createElement('span');
|
||
span.textContent = emoji;
|
||
span.style.cursor = 'pointer';
|
||
span.style.fontSize = '20px';
|
||
span.style.padding = '5px';
|
||
span.style.textAlign = 'center';
|
||
span.className = 'rounded role-emoji-item';
|
||
span.onclick = () => {
|
||
document.getElementById('edit-role-icon').value = emoji;
|
||
document.getElementById('selected-role-emoji-preview').textContent = emoji;
|
||
};
|
||
roleEmojiGrid.appendChild(span);
|
||
});
|
||
}
|
||
|
||
function initEmojiCategories() {
|
||
const categoriesContainer = document.getElementById('role-emoji-categories');
|
||
const searchInput = document.getElementById('role-emoji-search');
|
||
if (!categoriesContainer) return;
|
||
|
||
categoriesContainer.innerHTML = '';
|
||
Object.keys(EMOJI_CATEGORIES).forEach((cat, index) => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'btn btn-sm btn-dark text-nowrap px-2 py-1';
|
||
btn.style.fontSize = '0.75em';
|
||
btn.textContent = cat;
|
||
if (index === 0) btn.classList.add('active', 'btn-primary');
|
||
|
||
btn.onclick = () => {
|
||
if (searchInput) searchInput.value = '';
|
||
categoriesContainer.querySelectorAll('button').forEach(b => b.classList.remove('active', 'btn-primary'));
|
||
btn.classList.add('active', 'btn-primary');
|
||
populateEmojiGrid(cat);
|
||
};
|
||
categoriesContainer.appendChild(btn);
|
||
});
|
||
|
||
if (searchInput) {
|
||
searchInput.oninput = () => {
|
||
const term = searchInput.value.trim();
|
||
if (term) {
|
||
categoriesContainer.querySelectorAll('button').forEach(b => b.classList.remove('active', 'btn-primary'));
|
||
populateEmojiGrid(null, term);
|
||
} else {
|
||
const activeCat = categoriesContainer.querySelector('button.active')?.textContent || Object.keys(EMOJI_CATEGORIES)[0];
|
||
populateEmojiGrid(activeCat);
|
||
}
|
||
};
|
||
}
|
||
|
||
// Initial load
|
||
populateEmojiGrid(Object.keys(EMOJI_CATEGORIES)[0]);
|
||
}
|
||
|
||
// Call init if elements exist
|
||
if (document.getElementById('role-emoji-grid')) {
|
||
initEmojiCategories();
|
||
}
|
||
|
||
// Scroll to bottom
|
||
scrollToBottom(true);
|
||
|
||
const currentChannel = new URLSearchParams(window.location.search).get('channel_id') || 1;
|
||
const currentThread = new URLSearchParams(window.location.search).get('thread_id');
|
||
let typingTimeout;
|
||
|
||
// Notification Permission
|
||
if ("Notification" in window && Notification.permission === "default") {
|
||
Notification.requestPermission();
|
||
}
|
||
|
||
// WebSocket for real-time
|
||
let ws;
|
||
let voiceHandler;
|
||
|
||
function connectWS() {
|
||
try {
|
||
ws = new WebSocket('ws://' + window.location.hostname + ':8080');
|
||
|
||
if (typeof VoiceChannel !== 'undefined') {
|
||
voiceHandler = new VoiceChannel(ws);
|
||
}
|
||
|
||
ws.onmessage = (e) => {
|
||
const msg = JSON.parse(e.data);
|
||
|
||
// Voice signaling
|
||
if (msg.type && msg.type.startsWith('voice_')) {
|
||
if (voiceHandler) voiceHandler.handleSignaling(msg);
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'message') {
|
||
const data = JSON.parse(msg.data);
|
||
if (data.channel_id == currentChannel) {
|
||
appendMessage(data);
|
||
|
||
// Desktop Notifications for mentions
|
||
if (data.content.includes(`@${window.currentUsername}`) && data.user_id != window.currentUserId) {
|
||
if (Notification.permission === "granted" && !window.isDndMode) {
|
||
new Notification(`Mention in #${window.currentChannelName}`, {
|
||
body: `${data.username}: ${data.content}`,
|
||
icon: data.avatar_url || ''
|
||
});
|
||
}
|
||
}
|
||
}
|
||
} else if (msg.type === 'typing') {
|
||
if (msg.channel_id == currentChannel && msg.user_id != window.currentUserId) {
|
||
showTyping(msg.username);
|
||
}
|
||
} else if (msg.type === 'reaction') {
|
||
updateReactionUI(msg.message_id, msg.reactions);
|
||
} else if (msg.type === 'message_edit') {
|
||
const el = document.querySelector(`.message-item[data-id="${msg.message_id}"] .message-text`);
|
||
if (el) el.innerHTML = msg.content.replace(/\n/g, '<br>');
|
||
} else if (msg.type === 'message_delete') {
|
||
document.querySelector(`.message-item[data-id="${msg.message_id}"]`)?.remove();
|
||
} else if (msg.type === 'presence') {
|
||
updatePresenceUI(msg.user_id, msg.status);
|
||
}
|
||
};
|
||
ws.onopen = () => {
|
||
ws.send(JSON.stringify({
|
||
type: 'presence',
|
||
user_id: window.currentUserId,
|
||
status: 'online'
|
||
}));
|
||
};
|
||
ws.onclose = () => setTimeout(connectWS, 3000);
|
||
} catch (e) {
|
||
console.warn('WebSocket connection failed.');
|
||
}
|
||
}
|
||
connectWS();
|
||
|
||
// Polling as fallback for real-time
|
||
let lastMessageId = 0;
|
||
const findLastMessageId = () => {
|
||
const items = document.querySelectorAll('.message-item');
|
||
if (items.length > 0) {
|
||
lastMessageId = Math.max(...Array.from(items).map(i => parseInt(i.dataset.id) || 0));
|
||
}
|
||
};
|
||
findLastMessageId();
|
||
|
||
setInterval(async () => {
|
||
if (!currentChannel) return;
|
||
try {
|
||
const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&after_id=${lastMessageId}`);
|
||
const data = await resp.json();
|
||
if (data.success && data.messages && data.messages.length > 0) {
|
||
data.messages.forEach(msg => {
|
||
appendMessage(msg);
|
||
});
|
||
}
|
||
} catch (e) { }
|
||
}, 1000);
|
||
|
||
function showTyping(username) {
|
||
typingIndicator.textContent = `${username} is typing...`;
|
||
clearTimeout(typingTimeout);
|
||
typingTimeout = setTimeout(() => {
|
||
typingIndicator.textContent = '';
|
||
}, 3000);
|
||
}
|
||
|
||
chatInput.addEventListener('input', () => {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({
|
||
type: 'typing',
|
||
channel_id: currentChannel,
|
||
user_id: window.currentUserId,
|
||
username: window.currentUsername
|
||
}));
|
||
}
|
||
});
|
||
|
||
chatForm.addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
const content = chatInput.value.trim();
|
||
const file = fileUpload.files[0];
|
||
if (!content && !file) return;
|
||
|
||
chatInput.value = '';
|
||
const formData = new FormData();
|
||
formData.append('content', content);
|
||
formData.append('channel_id', currentChannel);
|
||
if (currentThread) {
|
||
formData.append('thread_id', currentThread);
|
||
}
|
||
|
||
const progressContainer = document.getElementById('upload-progress-container');
|
||
const progressBar = document.getElementById('upload-progress-bar');
|
||
const progressPercent = document.getElementById('upload-percentage');
|
||
const progressFilename = document.getElementById('upload-filename');
|
||
|
||
if (file) {
|
||
formData.append('file', file);
|
||
fileUpload.value = ''; // Clear file input
|
||
|
||
// Show progress bar
|
||
progressContainer.style.display = 'block';
|
||
progressFilename.textContent = `Uploading: ${file.name}`;
|
||
progressBar.style.width = '0%';
|
||
progressPercent.textContent = '0%';
|
||
}
|
||
|
||
const xhr = new XMLHttpRequest();
|
||
xhr.open('POST', 'api_v1_messages.php', true);
|
||
|
||
xhr.upload.onprogress = (ev) => {
|
||
if (ev.lengthComputable && file) {
|
||
const percent = Math.round((ev.loaded / ev.total) * 100);
|
||
progressBar.style.width = percent + '%';
|
||
progressPercent.textContent = percent + '%';
|
||
}
|
||
};
|
||
|
||
xhr.onload = () => {
|
||
if (xhr.status === 200) {
|
||
const result = JSON.parse(xhr.responseText);
|
||
if (result.success) {
|
||
appendMessage(result.message);
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({
|
||
type: 'message',
|
||
data: JSON.stringify({
|
||
...result.message,
|
||
channel_id: currentChannel
|
||
})
|
||
}));
|
||
}
|
||
} else {
|
||
alert(result.error || 'Failed to send message');
|
||
}
|
||
}
|
||
progressContainer.style.display = 'none';
|
||
};
|
||
|
||
xhr.onerror = () => {
|
||
console.error('XHR Error');
|
||
progressContainer.style.display = 'none';
|
||
alert('An error occurred during the upload.');
|
||
};
|
||
|
||
xhr.send(formData);
|
||
});
|
||
|
||
// Handle Reaction Clicks
|
||
document.addEventListener('click', (e) => {
|
||
const badge = e.target.closest('.reaction-badge');
|
||
if (badge) {
|
||
const msgId = badge.parentElement.dataset.messageId;
|
||
const emoji = badge.dataset.emoji;
|
||
toggleReaction(msgId, emoji);
|
||
return;
|
||
}
|
||
|
||
const addBtn = e.target.closest('.add-reaction-btn');
|
||
if (addBtn) {
|
||
const msgId = addBtn.parentElement.dataset.messageId;
|
||
showEmojiPicker(addBtn, (emoji) => toggleReaction(msgId, emoji));
|
||
return;
|
||
}
|
||
|
||
// Close picker if click outside
|
||
if (!e.target.closest('.emoji-picker')) {
|
||
const picker = document.querySelector('.emoji-picker');
|
||
if (picker) picker.remove();
|
||
}
|
||
});
|
||
|
||
async function toggleReaction(messageId, emoji) {
|
||
try {
|
||
const resp = await fetch('api_v1_reactions.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ message_id: messageId, emoji: emoji })
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({
|
||
type: 'reaction',
|
||
message_id: messageId,
|
||
reactions: result.reactions
|
||
}));
|
||
}
|
||
updateReactionUI(messageId, result.reactions);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function showEmojiPicker(anchor, callback) {
|
||
document.querySelector('.emoji-picker')?.remove();
|
||
const picker = document.createElement('div');
|
||
picker.className = 'emoji-picker p-0 overflow-hidden d-flex flex-column';
|
||
picker.style.width = '280px';
|
||
picker.style.height = '320px';
|
||
picker.style.backgroundColor = 'var(--bg-secondary)';
|
||
picker.style.border = '1px solid var(--bg-tertiary)';
|
||
picker.style.borderRadius = '8px';
|
||
picker.style.boxShadow = '0 8px 24px rgba(0,0,0,0.5)';
|
||
picker.style.zIndex = '2000';
|
||
|
||
const tabs = document.createElement('div');
|
||
tabs.className = 'd-flex overflow-auto border-bottom border-secondary p-1 bg-dark';
|
||
tabs.style.gap = '2px';
|
||
|
||
const searchContainer = document.createElement('div');
|
||
searchContainer.className = 'p-2 border-bottom border-secondary';
|
||
const searchInput = document.createElement('input');
|
||
searchInput.type = 'text';
|
||
searchInput.placeholder = 'Search emoji...';
|
||
searchInput.className = 'form-control form-control-sm bg-dark border-secondary text-white';
|
||
searchContainer.appendChild(searchInput);
|
||
|
||
const grid = document.createElement('div');
|
||
grid.className = 'flex-grow-1 overflow-auto p-2';
|
||
grid.style.display = 'grid';
|
||
grid.style.gridTemplateColumns = 'repeat(7, 1fr)';
|
||
grid.style.gap = '2px';
|
||
|
||
const renderGrid = (cat = null, term = '') => {
|
||
grid.innerHTML = '';
|
||
let emojis = [];
|
||
if (term) {
|
||
emojis = EMOJIS.filter(e => e.includes(term));
|
||
} else {
|
||
emojis = EMOJI_CATEGORIES[cat] || [];
|
||
}
|
||
|
||
emojis.forEach(emoji => {
|
||
const span = document.createElement('span');
|
||
span.textContent = emoji;
|
||
span.style.cursor = 'pointer';
|
||
span.style.fontSize = '20px';
|
||
span.style.padding = '5px';
|
||
span.style.textAlign = 'center';
|
||
span.className = 'rounded role-emoji-item';
|
||
span.onclick = () => {
|
||
callback(emoji);
|
||
picker.remove();
|
||
};
|
||
grid.appendChild(span);
|
||
});
|
||
};
|
||
|
||
searchInput.oninput = () => {
|
||
const term = searchInput.value.trim();
|
||
if (term) {
|
||
tabs.querySelectorAll('button').forEach(b => b.classList.remove('text-primary'));
|
||
renderGrid(null, term);
|
||
} else {
|
||
const activeCat = tabs.querySelector('button.text-primary')?.textContent || Object.keys(EMOJI_CATEGORIES)[0];
|
||
renderGrid(activeCat);
|
||
}
|
||
};
|
||
|
||
Object.keys(EMOJI_CATEGORIES).forEach((cat, index) => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'btn btn-sm btn-dark text-nowrap px-2 py-1 border-0';
|
||
btn.style.fontSize = '0.7em';
|
||
btn.textContent = cat;
|
||
if (index === 0) btn.classList.add('text-primary');
|
||
|
||
btn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
searchInput.value = '';
|
||
tabs.querySelectorAll('button').forEach(b => b.classList.remove('text-primary'));
|
||
btn.classList.add('text-primary');
|
||
renderGrid(cat);
|
||
};
|
||
tabs.appendChild(btn);
|
||
});
|
||
|
||
picker.appendChild(tabs);
|
||
picker.appendChild(searchContainer);
|
||
picker.appendChild(grid);
|
||
document.body.appendChild(picker);
|
||
|
||
renderGrid(Object.keys(EMOJI_CATEGORIES)[0]);
|
||
|
||
const rect = anchor.getBoundingClientRect();
|
||
let top = rect.top - picker.offsetHeight - 5;
|
||
if (top < 0) top = rect.bottom + 5;
|
||
picker.style.position = 'fixed';
|
||
picker.style.top = `${top}px`;
|
||
picker.style.left = `${Math.min(rect.left, window.innerWidth - 300)}px`;
|
||
}
|
||
|
||
function updateReactionUI(messageId, reactions) {
|
||
const container = document.querySelector(`.message-reactions[data-message-id="${messageId}"]`);
|
||
if (!container) return;
|
||
|
||
const addBtn = container.querySelector('.add-reaction-btn');
|
||
container.innerHTML = '';
|
||
reactions.forEach(r => {
|
||
const badge = document.createElement('span');
|
||
const userList = r.users.split(',');
|
||
const active = userList.includes(String(window.currentUserId));
|
||
badge.className = `reaction-badge ${active ? 'active' : ''}`;
|
||
badge.dataset.emoji = r.emoji;
|
||
badge.innerHTML = `${r.emoji} <span class="count">${r.count}</span>`;
|
||
container.appendChild(badge);
|
||
});
|
||
container.appendChild(addBtn);
|
||
}
|
||
|
||
function updatePresenceUI(userId, status) {
|
||
const memberItem = document.querySelector(`.start-dm-btn[data-user-id="${userId}"] .message-avatar`);
|
||
if (memberItem) {
|
||
let indicator = memberItem.querySelector('.presence-indicator');
|
||
if (!indicator) {
|
||
indicator = document.createElement('div');
|
||
indicator.className = 'presence-indicator';
|
||
memberItem.appendChild(indicator);
|
||
}
|
||
indicator.style.position = 'absolute';
|
||
indicator.style.bottom = '0';
|
||
indicator.style.right = '0';
|
||
indicator.style.width = '10px';
|
||
indicator.style.height = '10px';
|
||
indicator.style.borderRadius = '50%';
|
||
indicator.style.border = '2px solid var(--bg-members)';
|
||
indicator.style.backgroundColor = status === 'online' ? '#23a559' : '#80848e';
|
||
}
|
||
}
|
||
|
||
// Voice
|
||
if (voiceHandler) {
|
||
document.querySelectorAll('.voice-item').forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
const cid = item.dataset.channelId;
|
||
if (voiceHandler.currentChannelId == cid) {
|
||
voiceHandler.leave();
|
||
item.classList.remove('active');
|
||
} else {
|
||
voiceHandler.join(cid);
|
||
document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active'));
|
||
item.classList.add('active');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Message Actions (Edit/Delete)
|
||
document.addEventListener('click', async (e) => {
|
||
const editBtn = e.target.closest('.action-btn.edit');
|
||
if (editBtn) {
|
||
const msgId = editBtn.dataset.id;
|
||
const msgItem = editBtn.closest('.message-item');
|
||
const textEl = msgItem.querySelector('.message-text');
|
||
const originalContent = textEl.innerText;
|
||
|
||
const input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.className = 'form-control bg-dark text-white';
|
||
input.value = originalContent;
|
||
|
||
textEl.innerHTML = '';
|
||
textEl.appendChild(input);
|
||
input.focus();
|
||
|
||
input.onkeydown = async (ev) => {
|
||
if (ev.key === 'Enter') {
|
||
const newContent = input.value.trim();
|
||
if (newContent && newContent !== originalContent) {
|
||
const resp = await fetch('api_v1_messages.php', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: msgId, content: newContent })
|
||
});
|
||
if ((await resp.json()).success) {
|
||
textEl.innerHTML = newContent.replace(/\n/g, '<br>');
|
||
ws?.send(JSON.stringify({ type: 'message_edit', message_id: msgId, content: newContent }));
|
||
}
|
||
} else {
|
||
textEl.innerHTML = originalContent.replace(/\n/g, '<br>');
|
||
}
|
||
} else if (ev.key === 'Escape') {
|
||
textEl.innerHTML = originalContent.replace(/\n/g, '<br>');
|
||
}
|
||
};
|
||
return;
|
||
}
|
||
|
||
const deleteBtn = e.target.closest('.action-btn.delete');
|
||
if (deleteBtn) {
|
||
if (!confirm('Delete this message?')) return;
|
||
const msgId = deleteBtn.dataset.id;
|
||
const resp = await fetch('api_v1_messages.php', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: msgId })
|
||
});
|
||
if ((await resp.json()).success) {
|
||
deleteBtn.closest('.message-item').remove();
|
||
ws?.send(JSON.stringify({ type: 'message_delete', message_id: msgId }));
|
||
}
|
||
return;
|
||
}
|
||
|
||
const pinBtn = e.target.closest('.action-btn.pin');
|
||
if (pinBtn) {
|
||
const msgId = pinBtn.dataset.id;
|
||
const isPinned = pinBtn.dataset.pinned == '1';
|
||
const action = isPinned ? 'unpin' : 'pin';
|
||
|
||
const resp = await fetch('api_v1_messages.php', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: msgId, action: action })
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload(); // Simplest way to reflect changes across UI
|
||
}
|
||
return;
|
||
}
|
||
|
||
const pinnedMessagesBtn = document.getElementById('pinned-messages-btn');
|
||
if (e.target.closest('#pinned-messages-btn')) {
|
||
const container = document.getElementById('pinned-messages-container');
|
||
container.innerHTML = '<div class="p-3 text-center text-muted">Loading pinned messages...</div>';
|
||
const modal = new bootstrap.Modal(document.getElementById('pinnedMessagesModal'));
|
||
modal.show();
|
||
|
||
const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&pinned=1`);
|
||
const data = await resp.json();
|
||
if (data.success && data.messages.length > 0) {
|
||
container.innerHTML = '';
|
||
data.messages.forEach(msg => {
|
||
const div = document.createElement('div');
|
||
div.className = 'message-item p-2 border-bottom border-secondary';
|
||
div.style.backgroundColor = 'transparent';
|
||
const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
|
||
div.innerHTML = `
|
||
<div class="d-flex align-items-start">
|
||
<div class="message-avatar" style="width: 32px; height: 32px; margin-right: 10px; ${msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : ''}"></div>
|
||
<div style="flex: 1;">
|
||
<div class="message-author" style="font-size: 0.85em; ${authorStyle}">
|
||
${escapeHTML(msg.username)}
|
||
${msg.role_icon ? `<img src="${msg.role_icon}" class="role-icon ms-1" style="width: 12px; height: 12px; vertical-align: middle; object-fit: contain;">` : ''}
|
||
<span class="message-time">${msg.time}</span>
|
||
</div>
|
||
<div class="message-text" style="font-size: 0.9em;">
|
||
${escapeHTML(msg.content).replace(/\n/g, '<br>')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
container.appendChild(div);
|
||
});
|
||
} else {
|
||
container.innerHTML = '<div class="p-3 text-center text-muted">No pinned messages in this channel.</div>';
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 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;
|
||
|
||
// 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 = '150px';
|
||
|
||
const rect = memberItem.getBoundingClientRect();
|
||
menu.style.top = `${rect.top}px`;
|
||
menu.style.left = `${rect.left - 160}px`;
|
||
|
||
menu.innerHTML = `
|
||
<div class="mb-2 p-1 border-bottom border-secondary d-flex align-items-center">
|
||
<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>
|
||
<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 member-menu-action" data-action="edit-roles">รditer son rรดle</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);
|
||
}
|
||
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">${res.username}</div>
|
||
<div class="search-result-text">${res.content}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
searchResults.appendChild(item);
|
||
});
|
||
searchResults.style.display = 'block';
|
||
} else {
|
||
searchResults.innerHTML = '<div class="p-2 text-muted">No results found</div>';
|
||
searchResults.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
// Channel Permissions Management
|
||
const channelPermissionsTabBtn = document.getElementById('channel-permissions-tab-btn');
|
||
const channelPermissionsList = document.getElementById('channel-permissions-list');
|
||
const addPermRoleList = document.getElementById('add-permission-role-list');
|
||
|
||
channelPermissionsTabBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
loadChannelPermissions(channelId);
|
||
loadRolesForPermissions(channelId);
|
||
});
|
||
|
||
async function loadChannelPermissions(channelId) {
|
||
channelPermissionsList.innerHTML = '<div class="text-center p-3 text-muted small">Loading permissions...</div>';
|
||
const resp = await fetch(`api_v1_channel_permissions.php?channel_id=${channelId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
renderChannelPermissions(channelId, data.permissions);
|
||
}
|
||
}
|
||
|
||
async function loadRolesForPermissions(channelId) {
|
||
addPermRoleList.innerHTML = '';
|
||
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
data.roles.forEach(role => {
|
||
const li = document.createElement('li');
|
||
li.innerHTML = `<a class="dropdown-item d-flex align-items-center gap-2" href="#">
|
||
<div style="width: 10px; height: 10px; border-radius: 50%; background-color: ${role.color};"></div>
|
||
${role.name}
|
||
</a>`;
|
||
li.onclick = async (e) => {
|
||
e.preventDefault();
|
||
await fetch('api_v1_channel_permissions.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ channel_id: channelId, role_id: role.id, allow: 0, deny: 0 })
|
||
});
|
||
loadChannelPermissions(channelId);
|
||
};
|
||
addPermRoleList.appendChild(li);
|
||
});
|
||
}
|
||
}
|
||
|
||
function renderChannelPermissions(channelId, permissions) {
|
||
channelPermissionsList.innerHTML = '';
|
||
if (permissions.length === 0) {
|
||
channelPermissionsList.innerHTML = '<div class="text-center p-3 text-muted small">No role overrides.</div>';
|
||
return;
|
||
}
|
||
permissions.forEach(p => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary p-2';
|
||
item.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<div class="d-flex align-items-center">
|
||
<div style="width: 10px; height: 10px; border-radius: 50%; background-color: ${p.role_color}; margin-right: 8px;"></div>
|
||
<span class="small fw-bold">${p.role_name}</span>
|
||
</div>
|
||
<button class="btn btn-sm text-danger remove-perm-btn" data-role-id="${p.role_id}">ร</button>
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
<select class="form-select form-select-sm bg-dark text-white border-secondary perm-select" data-role-id="${p.role_id}">
|
||
<option value="allow" ${p.allow_permissions ? 'selected' : ''}>Allow Sending Messages</option>
|
||
<option value="deny" ${p.deny_permissions ? 'selected' : ''}>Deny Sending Messages</option>
|
||
<option value="neutral" ${!p.allow_permissions && !p.deny_permissions ? 'selected' : ''}>Neutral</option>
|
||
</select>
|
||
</div>
|
||
`;
|
||
channelPermissionsList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
channelPermissionsList?.addEventListener('click', async (e) => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
if (e.target.classList.contains('remove-perm-btn')) {
|
||
const roleId = e.target.dataset.roleId;
|
||
await fetch('api_v1_channel_permissions.php', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ channel_id: channelId, role_id: roleId })
|
||
});
|
||
loadChannelPermissions(channelId);
|
||
}
|
||
});
|
||
|
||
channelPermissionsList?.addEventListener('change', async (e) => {
|
||
if (e.target.classList.contains('perm-select')) {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
const roleId = e.target.dataset.roleId;
|
||
const val = e.target.value;
|
||
let allow = 0, deny = 0;
|
||
if (val === 'allow') allow = 1;
|
||
if (val === 'deny') deny = 1;
|
||
|
||
await fetch('api_v1_channel_permissions.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ channel_id: channelId, role_id: roleId, allow, deny })
|
||
});
|
||
}
|
||
});
|
||
|
||
document.addEventListener('click', async (e) => {
|
||
if (!e.target.closest('.search-container')) {
|
||
searchResults.style.display = 'none';
|
||
}
|
||
|
||
if (e.target.classList.contains('move-rule-btn')) {
|
||
const id = e.target.dataset.id;
|
||
const dir = e.target.dataset.dir;
|
||
const resp = await fetch('api_v1_rules.php', {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id, dir })
|
||
});
|
||
if ((await resp.json()).success) location.reload();
|
||
}
|
||
});
|
||
|
||
// Roles Management
|
||
const channelSettingsBtns = document.querySelectorAll('.channel-settings-btn');
|
||
channelSettingsBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const modal = document.getElementById('editChannelModal');
|
||
const channelId = btn.dataset.id;
|
||
const channelType = btn.dataset.type || 'chat';
|
||
|
||
modal.querySelector('#edit-channel-id').value = channelId;
|
||
modal.querySelector('#edit-channel-name').value = btn.dataset.name;
|
||
modal.querySelector('#edit-channel-type').value = channelType;
|
||
modal.querySelector('#edit-channel-files').checked = btn.dataset.files == '1';
|
||
modal.querySelector('#edit-channel-limit').value = btn.dataset.limit || '';
|
||
modal.querySelector('#edit-channel-status').value = btn.dataset.status || '';
|
||
modal.querySelector('#edit-channel-icon').value = btn.dataset.icon || '';
|
||
modal.querySelector('#delete-channel-id').value = channelId;
|
||
|
||
// Show/Hide RSS tab
|
||
const rssTabNav = document.getElementById('rss-tab-nav');
|
||
const statusContainer = document.getElementById('edit-channel-status-container');
|
||
|
||
if (channelType === 'announcement') {
|
||
rssTabNav.style.display = 'block';
|
||
} else {
|
||
rssTabNav.style.display = 'none';
|
||
// Switch to General tab if we were on RSS
|
||
if (document.getElementById('rss-tab-btn').classList.contains('active')) {
|
||
bootstrap.Tab.getInstance(modal.querySelector('.nav-link.active')).hide();
|
||
bootstrap.Tab.getOrCreateInstance(modal.querySelector('[data-bs-target="#edit-channel-general"]')).show();
|
||
}
|
||
}
|
||
|
||
if (channelType === 'voice') {
|
||
statusContainer.style.display = 'block';
|
||
} else {
|
||
statusContainer.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
|
||
// RSS Management
|
||
const editChannelType = document.getElementById('edit-channel-type');
|
||
editChannelType?.addEventListener('change', () => {
|
||
const type = editChannelType.value;
|
||
const rssTabNav = document.getElementById('rss-tab-nav');
|
||
const statusContainer = document.getElementById('edit-channel-status-container');
|
||
|
||
rssTabNav.style.display = (type === 'announcement') ? 'block' : 'none';
|
||
statusContainer.style.display = (type === 'voice') ? 'block' : 'none';
|
||
});
|
||
|
||
// RSS Management
|
||
const rssTabBtn = document.getElementById('rss-tab-btn');
|
||
const rssFeedsList = document.getElementById('rss-feeds-list');
|
||
const addRssBtn = document.getElementById('add-rss-btn');
|
||
const syncRssBtn = document.getElementById('sync-rss-btn');
|
||
|
||
rssTabBtn?.addEventListener('click', loadRssFeeds);
|
||
|
||
async function loadRssFeeds() {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
rssFeedsList.innerHTML = '<div class="text-center p-3 text-muted small">Loading feeds...</div>';
|
||
try {
|
||
const resp = await fetch(`api_v1_rss.php?channel_id=${channelId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
renderRssFeeds(data.feeds);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function renderRssFeeds(feeds) {
|
||
rssFeedsList.innerHTML = '';
|
||
if (feeds.length === 0) {
|
||
rssFeedsList.innerHTML = '<div class="text-center p-3 text-muted small">No RSS feeds configured.</div>';
|
||
return;
|
||
}
|
||
feeds.forEach(feed => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-1';
|
||
item.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||
<span class="small text-truncate" style="max-width: 80%;">${feed.url}</span>
|
||
<button class="btn btn-sm text-danger delete-rss-btn" data-id="${feed.id}">ร</button>
|
||
</div>
|
||
<div class="small text-muted" style="font-size: 0.7em;">Last fetched: ${feed.last_fetched_at || 'Never'}</div>
|
||
`;
|
||
rssFeedsList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
addRssBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
const url = document.getElementById('new-rss-url').value.trim();
|
||
if (!url) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('action', 'add');
|
||
formData.append('channel_id', channelId);
|
||
formData.append('url', url);
|
||
|
||
const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData });
|
||
if ((await resp.json()).success) {
|
||
document.getElementById('new-rss-url').value = '';
|
||
loadRssFeeds();
|
||
}
|
||
});
|
||
|
||
syncRssBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
syncRssBtn.disabled = true;
|
||
syncRssBtn.textContent = 'Syncing...';
|
||
|
||
const formData = new FormData();
|
||
formData.append('action', 'sync');
|
||
formData.append('channel_id', channelId);
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData });
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
alert(`Sync complete! Found ${result.new_items} new items.`);
|
||
loadRssFeeds();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
|
||
syncRssBtn.disabled = false;
|
||
syncRssBtn.textContent = 'Sync Now';
|
||
});
|
||
|
||
rssFeedsList?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-rss-btn')) {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
const feedId = e.target.dataset.id;
|
||
const formData = new FormData();
|
||
formData.append('action', 'delete');
|
||
formData.append('channel_id', channelId);
|
||
formData.append('feed_id', feedId);
|
||
|
||
await fetch('api_v1_rss.php', { method: 'POST', body: formData });
|
||
loadRssFeeds();
|
||
}
|
||
});
|
||
|
||
// Clear Channel History
|
||
const clearHistoryBtn = document.getElementById('clear-channel-history-btn');
|
||
clearHistoryBtn?.addEventListener('click', async () => {
|
||
const channelId = document.getElementById('edit-channel-id').value;
|
||
if (!confirm('Voulez-vous vraiment vider tout l\'historique de ce salon ? Cette action est irrรฉversible.')) return;
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('channel_id', channelId);
|
||
const resp = await fetch('api_v1_clear_channel.php', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(result.error || 'Erreur lors du nettoyage de l\'historique');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Roles Management
|
||
const rolesTabBtn = document.getElementById('roles-tab-btn');
|
||
const rolesList = document.getElementById('roles-list');
|
||
const addRoleBtn = document.getElementById('add-role-btn');
|
||
const membersTabBtn = document.getElementById('members-tab-btn');
|
||
const membersList = document.getElementById('server-members-list');
|
||
const activeServerId = 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() {
|
||
if (rolesList) rolesList.innerHTML = '<div class="text-center p-3 text-muted">Loading...</div>';
|
||
if (membersList) membersList.innerHTML = '<div class="text-center p-3 text-muted">Loading...</div>';
|
||
|
||
try {
|
||
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
serverRoles = data.roles;
|
||
serverPermissions = data.permissions_list;
|
||
if (rolesList) renderRoles(data.roles);
|
||
if (membersList) renderMembers(data.members);
|
||
updateGlobalUI(data.members);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function renderRoleIconJS(icon, size = '12px') {
|
||
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 {
|
||
return `<span class="ms-1" style="font-size: ${size}; vertical-align: middle;">${escapeHTML(icon)}</span>`;
|
||
}
|
||
}
|
||
|
||
function updateGlobalUI(members) {
|
||
// 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 โ ${members.length}`;
|
||
|
||
// We need to keep the "Members - X" div and replace everything else
|
||
const header = sidebar.firstElementChild;
|
||
sidebar.innerHTML = '';
|
||
sidebar.appendChild(header);
|
||
|
||
members.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.style.color = 'var(--text-primary)';
|
||
item.style.marginBottom = '8px';
|
||
item.style.cursor = 'pointer';
|
||
|
||
const roleIconHtml = renderRoleIconJS(m.role_icon, '12px');
|
||
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 = members.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, '12px');
|
||
|
||
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 isUrl = role.icon_url && (role.icon_url.startsWith('http') || role.icon_url.startsWith('/'));
|
||
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>
|
||
${role.icon_url ? (isUrl ? `<img src="${role.icon_url}" class="ms-1" style="width: 12px; height: 12px; object-fit: contain;">` : `<span class="ms-1" style="font-size: 12px;">${role.icon_url}</span>`) : ''}
|
||
</div>
|
||
<div>
|
||
<button class="btn btn-sm btn-outline-light edit-role-btn-v2" data-id="${role.id}" data-name="${role.name}" data-color="${role.color}" data-perms="${role.permissions}" 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 isIconUrl = member.role_icon && (member.role_icon.startsWith('http') || member.role_icon.startsWith('/'));
|
||
const roleIconHtml = member.role_icon ? (isIconUrl ? `<img src="${member.role_icon}" class="role-icon ms-1" style="width: 12px; height: 12px; vertical-align: middle; object-fit: contain;">` : `<span class="ms-1" style="font-size: 12px; vertical-align: middle;">${member.role_icon}</span>`) : '';
|
||
|
||
item.innerHTML = `
|
||
<div class="d-flex align-items-center flex-grow-1">
|
||
<div class="message-avatar me-2" style="width: 32px; height: 32px; ${member.avatar_url ? `background-image: url('${member.avatar_url}');` : ''}"></div>
|
||
<div class="flex-grow-1">
|
||
<div class="fw-bold small" style="color: ${member.role_color || 'inherit'}">
|
||
${escapeHTML(member.username)}
|
||
${roleIconHtml}
|
||
</div>
|
||
<div class="text-muted small">
|
||
${member.role_names ? member.role_names.split(',').join(', ') : 'No roles'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
${(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>
|
||
` : ''}
|
||
`;
|
||
membersList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
// Add listener for the button in members list tab
|
||
membersList?.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.edit-user-roles-settings-btn');
|
||
if (btn) {
|
||
openEditUserRolesModal(btn.dataset.id, btn.dataset.username, btn.dataset.avatar);
|
||
}
|
||
});
|
||
|
||
|
||
// Role Editing Modal Logic
|
||
rolesList?.addEventListener('click', (e) => {
|
||
if (e.target.classList.contains('edit-role-btn-v2')) {
|
||
const role = e.target.dataset;
|
||
document.getElementById('edit-role-id').value = role.id;
|
||
document.getElementById('edit-role-name').value = role.name;
|
||
document.getElementById('edit-role-color').value = role.color;
|
||
document.getElementById('edit-role-icon').value = role.icon;
|
||
document.getElementById('selected-role-emoji-preview').textContent = role.icon || '';
|
||
|
||
const 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 resp = await fetch('api_v1_roles.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'update', server_id: activeServerId, id, name, color, 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 () => {
|
||
const name = prompt('Role name:');
|
||
if (!name) return;
|
||
const color = '#99aab5';
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_roles.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'create', server_id: activeServerId, name, color, permissions: 0 })
|
||
});
|
||
if ((await resp.json()).success) loadRoles();
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
rolesList?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-role-btn')) {
|
||
if (!confirm('Delete this role?')) return;
|
||
const roleId = e.target.dataset.id;
|
||
const resp = await fetch('api_v1_roles.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', server_id: activeServerId, id: roleId })
|
||
});
|
||
if ((await resp.json()).success) loadRoles();
|
||
}
|
||
});
|
||
|
||
// Webhooks Management
|
||
const webhooksTabBtn = document.getElementById('webhooks-tab-btn');
|
||
const webhooksList = document.getElementById('webhooks-list');
|
||
const addWebhookBtn = document.getElementById('add-webhook-btn');
|
||
|
||
webhooksTabBtn?.addEventListener('click', loadWebhooks);
|
||
|
||
async function loadWebhooks() {
|
||
webhooksList.innerHTML = '<div class="text-center p-3 text-muted">Loading webhooks...</div>';
|
||
try {
|
||
const resp = await fetch(`api_v1_webhook.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
renderWebhooks(data.webhooks);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function renderWebhooks(webhooks) {
|
||
webhooksList.innerHTML = '';
|
||
if (webhooks.length === 0) {
|
||
webhooksList.innerHTML = '<div class="text-center p-3 text-muted">No webhooks found.</div>';
|
||
return;
|
||
}
|
||
webhooks.forEach(wh => {
|
||
const item = document.createElement('div');
|
||
item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-2';
|
||
const url = `${window.location.origin}/api_v1_webhook.php?token=${wh.token}`;
|
||
item.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||
<span class="fw-bold">${wh.name}</span>
|
||
<button class="btn btn-sm btn-outline-danger delete-webhook-btn" data-id="${wh.id}">ร</button>
|
||
</div>
|
||
<div class="small text-muted mb-2">Channel: #${wh.channel_name}</div>
|
||
<div class="input-group input-group-sm">
|
||
<input type="text" class="form-control bg-dark text-white border-secondary" value="${url}" readonly>
|
||
<button class="btn btn-outline-secondary" type="button" onclick="navigator.clipboard.writeText('${url}')">Copy</button>
|
||
</div>
|
||
`;
|
||
webhooksList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
addWebhookBtn?.addEventListener('click', async () => {
|
||
const name = prompt('Webhook name:', 'Bot Name');
|
||
if (!name) return;
|
||
|
||
// Fetch channels for this server to let user pick one
|
||
const respChannels = await fetch(`api_v1_channels.php?server_id=${activeServerId}`);
|
||
const dataChannels = await respChannels.json();
|
||
if (!dataChannels.length) return alert('Create a channel first.');
|
||
|
||
const channelId = prompt('Enter Channel ID:\n' + dataChannels.map(c => `${c.id}: #${c.name}`).join('\n'));
|
||
if (!channelId) return;
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_webhook.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ channel_id: channelId, name: name })
|
||
});
|
||
if ((await resp.json()).success) loadWebhooks();
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
webhooksList?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-webhook-btn')) {
|
||
if (!confirm('Delete this webhook?')) return;
|
||
const whId = e.target.dataset.id;
|
||
const resp = await fetch('api_v1_webhook.php', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: whId })
|
||
});
|
||
if ((await resp.json()).success) loadWebhooks();
|
||
}
|
||
});
|
||
|
||
// Stats Management
|
||
const statsTabBtn = document.getElementById('stats-tab-btn');
|
||
statsTabBtn?.addEventListener('click', loadStats);
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const resp = await fetch(`api_v1_stats.php?server_id=${activeServerId}`);
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
document.getElementById('stat-members').textContent = data.stats.total_members;
|
||
document.getElementById('stat-messages').textContent = data.stats.total_messages;
|
||
|
||
const topUsersList = document.getElementById('top-users-list');
|
||
topUsersList.innerHTML = '';
|
||
data.stats.top_users.forEach(user => {
|
||
const item = document.createElement('div');
|
||
item.className = 'd-flex justify-content-between align-items-center mb-1 p-2 bg-dark rounded';
|
||
item.innerHTML = `<span>${user.username}</span><span class="badge bg-primary">${user.message_count} msgs</span>`;
|
||
topUsersList.appendChild(item);
|
||
});
|
||
|
||
const activity = document.getElementById('activity-chart-placeholder');
|
||
activity.innerHTML = '';
|
||
data.stats.history.forEach(day => {
|
||
const bar = document.createElement('div');
|
||
bar.className = 'd-flex align-items-center mb-1';
|
||
const percent = Math.min(100, (day.count / 100) * 100); // Normalize to 100 for visual
|
||
bar.innerHTML = `
|
||
<div style="width: 80px;" class="small">${day.date}</div>
|
||
<div class="flex-grow-1 mx-2" style="height: 10px; background: #1e1f22; border-radius: 5px;">
|
||
<div style="width: ${percent}%; height: 100%; background: var(--blurple); border-radius: 5px;"></div>
|
||
</div>
|
||
<div style="width: 30px;" class="small text-end">${day.count}</div>
|
||
`;
|
||
activity.appendChild(bar);
|
||
});
|
||
if (data.stats.history.length === 0) {
|
||
activity.innerHTML = '<div class="text-muted">No activity in the last 7 days.</div>';
|
||
}
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
// Server Settings
|
||
const searchServerIconBtn = document.getElementById('search-server-icon-btn');
|
||
const serverIconResults = document.getElementById('server-icon-search-results');
|
||
const serverIconPreview = document.getElementById('server-icon-preview');
|
||
const serverIconUrlInput = document.getElementById('server-icon-url');
|
||
|
||
searchServerIconBtn?.addEventListener('click', async () => {
|
||
const query = prompt('Search for a server icon:', 'abstract');
|
||
if (!query) return;
|
||
|
||
serverIconResults.innerHTML = '<div class="text-muted small">Searching...</div>';
|
||
try {
|
||
const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(query)}`);
|
||
const data = await resp.json();
|
||
serverIconResults.innerHTML = '';
|
||
data.forEach(photo => {
|
||
const img = document.createElement('img');
|
||
img.src = photo.url;
|
||
img.className = 'avatar-pick';
|
||
img.style.width = '50px';
|
||
img.style.height = '50px';
|
||
img.onclick = () => {
|
||
serverIconUrlInput.value = photo.url;
|
||
serverIconPreview.style.backgroundImage = `url('${photo.url}')`;
|
||
serverIconResults.innerHTML = '';
|
||
};
|
||
serverIconResults.appendChild(img);
|
||
});
|
||
} catch (e) {
|
||
serverIconResults.innerHTML = '<div class="text-danger small">Error fetching icons</div>';
|
||
}
|
||
});
|
||
|
||
// Forum: New Thread
|
||
const newThreadBtn = document.getElementById('new-thread-btn');
|
||
const newThreadModal = document.getElementById('newThreadModal') ? new bootstrap.Modal(document.getElementById('newThreadModal')) : null;
|
||
let selectedTagIds = [];
|
||
|
||
newThreadBtn?.addEventListener('click', async () => {
|
||
if (!newThreadModal) return;
|
||
|
||
// Load tags for this channel
|
||
const tagsList = document.getElementById('new-thread-tags-list');
|
||
tagsList.innerHTML = '<div class="text-muted small">Loading tags...</div>';
|
||
selectedTagIds = [];
|
||
|
||
try {
|
||
const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`);
|
||
const data = await resp.json();
|
||
tagsList.innerHTML = '';
|
||
if (data.success && data.tags.length > 0) {
|
||
data.tags.forEach(tag => {
|
||
const span = document.createElement('span');
|
||
span.className = 'badge rounded-pill p-2 border border-secondary';
|
||
span.style.cursor = 'pointer';
|
||
span.style.backgroundColor = 'transparent';
|
||
span.dataset.id = tag.id;
|
||
span.dataset.color = tag.color;
|
||
span.textContent = tag.name;
|
||
span.onclick = () => {
|
||
if (selectedTagIds.includes(tag.id)) {
|
||
selectedTagIds = selectedTagIds.filter(id => id !== tag.id);
|
||
span.style.backgroundColor = 'transparent';
|
||
} else {
|
||
selectedTagIds.push(tag.id);
|
||
span.style.backgroundColor = tag.color;
|
||
}
|
||
};
|
||
tagsList.appendChild(span);
|
||
});
|
||
} else {
|
||
tagsList.innerHTML = '<div class="text-muted small">No tags available.</div>';
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
|
||
newThreadModal.show();
|
||
});
|
||
|
||
document.getElementById('submit-new-thread-btn')?.addEventListener('click', async () => {
|
||
const title = document.getElementById('new-thread-title').value.trim();
|
||
if (!title) return;
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('channel_id', currentChannel);
|
||
formData.append('title', title);
|
||
formData.append('tag_ids', selectedTagIds.join(','));
|
||
const resp = await fetch('api_v1_threads.php', { method: 'POST', body: formData });
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
window.location.href = `?server_id=${activeServerId}&channel_id=${currentChannel}&thread_id=${result.thread_id}`;
|
||
} else {
|
||
alert(result.error || 'Failed to create thread');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Forum: Mark as Solution
|
||
document.addEventListener('click', async (e) => {
|
||
const solBtn = e.target.closest('.action-btn.mark-solution');
|
||
if (solBtn) {
|
||
const threadId = solBtn.dataset.threadId;
|
||
const messageId = solBtn.classList.contains('active') ? null : solBtn.dataset.messageId;
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_threads.php?action=solve', {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ thread_id: threadId, message_id: messageId })
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(result.error || 'Failed to update solution');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
});
|
||
|
||
// Forum: Manage Tags
|
||
const manageTagsBtn = document.getElementById('manage-tags-btn');
|
||
const manageTagsModal = document.getElementById('manageTagsModal') ? new bootstrap.Modal(document.getElementById('manageTagsModal')) : null;
|
||
|
||
manageTagsBtn?.addEventListener('click', async () => {
|
||
if (!manageTagsModal) return;
|
||
loadForumAdminTags();
|
||
manageTagsModal.show();
|
||
});
|
||
|
||
async function loadForumAdminTags() {
|
||
const list = document.getElementById('forum-tags-admin-list');
|
||
list.innerHTML = '<div class="text-center p-3 text-muted small">Loading tags...</div>';
|
||
try {
|
||
const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`);
|
||
const data = await resp.json();
|
||
list.innerHTML = '';
|
||
if (data.success && data.tags.length > 0) {
|
||
data.tags.forEach(tag => {
|
||
const div = document.createElement('div');
|
||
div.className = 'd-flex justify-content-between align-items-center mb-2 p-2 bg-dark rounded';
|
||
div.innerHTML = `
|
||
<div class="d-flex align-items-center">
|
||
<div style="width: 12px; height: 12px; border-radius: 50%; background-color: ${tag.color}; margin-right: 8px;"></div>
|
||
<span>${tag.name}</span>
|
||
</div>
|
||
<button class="btn btn-sm text-danger delete-forum-tag-btn" data-id="${tag.id}">ร</button>
|
||
`;
|
||
list.appendChild(div);
|
||
});
|
||
} else {
|
||
list.innerHTML = '<div class="text-center p-3 text-muted small">No tags created yet.</div>';
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
document.getElementById('add-forum-tag-btn')?.addEventListener('click', async () => {
|
||
const name = document.getElementById('new-tag-name').value.trim();
|
||
const color = document.getElementById('new-tag-color').value;
|
||
if (!name) return;
|
||
|
||
try {
|
||
const resp = await fetch('api_v1_tags.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'create', channel_id: currentChannel, name, color })
|
||
});
|
||
if ((await resp.json()).success) {
|
||
document.getElementById('new-tag-name').value = '';
|
||
loadForumAdminTags();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
document.getElementById('forum-tags-admin-list')?.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-forum-tag-btn')) {
|
||
const tagId = e.target.dataset.id;
|
||
try {
|
||
const resp = await fetch('api_v1_tags.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', channel_id: currentChannel, tag_id: tagId })
|
||
});
|
||
if ((await resp.json()).success) loadForumAdminTags();
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
});
|
||
|
||
// Rules: Add Rule
|
||
const addRuleBtn = document.getElementById('add-rule-btn');
|
||
addRuleBtn?.addEventListener('click', async () => {
|
||
const content = prompt('Rule Content:');
|
||
if (!content) return;
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('channel_id', currentChannel);
|
||
formData.append('content', content);
|
||
const resp = await fetch('api_v1_rules.php', { method: 'POST', body: formData });
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(result.error || 'Failed to add rule');
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Rules: Delete/Edit
|
||
document.addEventListener('click', async (e) => {
|
||
if (e.target.classList.contains('delete-rule-btn')) {
|
||
if (!confirm('Delete this rule?')) return;
|
||
const id = e.target.dataset.id;
|
||
const resp = await fetch(`api_v1_rules.php?id=${id}`, { method: 'DELETE' });
|
||
if ((await resp.json()).success) location.reload();
|
||
}
|
||
if (e.target.classList.contains('edit-rule-btn')) {
|
||
const id = e.target.dataset.id;
|
||
const oldContent = e.target.closest('.rule-item').querySelector('.rule-content').innerText;
|
||
const newContent = prompt('Edit Rule:', oldContent);
|
||
if (!newContent || newContent === oldContent) return;
|
||
|
||
const resp = await fetch('api_v1_rules.php', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id, content: newContent })
|
||
});
|
||
if ((await resp.json()).success) location.reload();
|
||
}
|
||
});
|
||
|
||
// Channel Selection Type
|
||
const addChannelBtns = document.querySelectorAll('.add-channel-btn');
|
||
addChannelBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const type = btn.dataset.type;
|
||
const select = document.getElementById('channel-type-select');
|
||
if (select) {
|
||
select.value = type === 'voice' ? 'voice' : 'chat';
|
||
}
|
||
});
|
||
});
|
||
|
||
// User Settings - Avatar Search
|
||
const avatarSearchBtn = document.getElementById('search-avatar-btn');
|
||
const avatarSearchQuery = document.getElementById('avatar-search-query');
|
||
const avatarResults = document.getElementById('avatar-results');
|
||
const avatarPreview = document.getElementById('settings-avatar-preview');
|
||
const avatarUrlInput = document.getElementById('settings-avatar-url');
|
||
|
||
avatarSearchBtn?.addEventListener('click', async () => {
|
||
const q = avatarSearchQuery.value.trim();
|
||
if (!q) return;
|
||
avatarResults.innerHTML = '<div class="text-muted small">Searching...</div>';
|
||
try {
|
||
const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(q)}`);
|
||
const data = await resp.json();
|
||
avatarResults.innerHTML = '';
|
||
data.forEach(photo => {
|
||
const img = document.createElement('img');
|
||
img.src = photo.url;
|
||
img.className = 'avatar-pick';
|
||
img.style.width = '60px';
|
||
img.style.height = '60px';
|
||
img.style.cursor = 'pointer';
|
||
img.onclick = () => {
|
||
avatarUrlInput.value = photo.url;
|
||
avatarPreview.style.backgroundImage = `url('${photo.url}')`;
|
||
};
|
||
avatarResults.appendChild(img);
|
||
});
|
||
} catch (e) { console.error(e); }
|
||
});
|
||
|
||
// Toggle members sidebar
|
||
const toggleMembersBtn = document.getElementById('toggle-members-btn');
|
||
const membersSidebar = document.querySelector('.members-sidebar');
|
||
if (toggleMembersBtn && membersSidebar) {
|
||
toggleMembersBtn.addEventListener('click', () => {
|
||
if (window.innerWidth > 992) {
|
||
membersSidebar.classList.toggle('hidden');
|
||
} else {
|
||
membersSidebar.classList.toggle('show');
|
||
}
|
||
});
|
||
}
|
||
|
||
// User Settings - Save
|
||
const saveSettingsBtn = document.getElementById('save-settings-btn');
|
||
saveSettingsBtn?.addEventListener('click', async () => {
|
||
const form = document.getElementById('user-settings-form');
|
||
const formData = new FormData(form);
|
||
const dndMode = document.getElementById('dnd-switch').checked ? '1' : '0';
|
||
formData.append('dnd_mode', dndMode);
|
||
|
||
const theme = form.querySelector('input[name="theme"]:checked').value;
|
||
document.body.setAttribute('data-theme', theme);
|
||
|
||
const resp = await fetch('api_v1_user.php', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const result = await resp.json();
|
||
if (result.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(result.error || 'Failed to save settings');
|
||
}
|
||
});
|
||
|
||
function escapeHTML(str) {
|
||
const div = document.createElement('div');
|
||
div.textContent = str;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function appendMessage(msg) {
|
||
if (!msg || !msg.id) return;
|
||
if (document.querySelector(`.message-item[data-id="${msg.id}"]`)) return;
|
||
|
||
// Auto-populate metadata for video platforms if missing
|
||
const dmRegexForMeta = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/;
|
||
const dmMatchForMeta = msg.content.match(dmRegexForMeta);
|
||
if (dmMatchForMeta && !msg.metadata) {
|
||
msg.metadata = {
|
||
title: 'Dailymotion Video',
|
||
url: dmMatchForMeta[0],
|
||
image: `https://www.dailymotion.com/thumbnail/video/${dmMatchForMeta[1]}`,
|
||
site_name: 'Dailymotion'
|
||
};
|
||
}
|
||
|
||
const messagesList = document.getElementById('messages-list');
|
||
const div = document.createElement('div');
|
||
div.className = 'message-item';
|
||
div.dataset.id = msg.id;
|
||
|
||
if (parseInt(msg.id) > lastMessageId) {
|
||
lastMessageId = parseInt(msg.id);
|
||
}
|
||
const avatarStyle = msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : '';
|
||
|
||
let attachmentHtml = '';
|
||
if (msg.attachment_url) {
|
||
const ext = msg.attachment_url.split('.').pop().toLowerCase();
|
||
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
|
||
attachmentHtml = `<div class="message-attachment mt-2"><img src="${msg.attachment_url}" class="img-fluid rounded message-img-preview" alt="Attachment" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)"></div>`;
|
||
} else {
|
||
attachmentHtml = `<div class="message-attachment mt-2"><a href="${msg.attachment_url}" target="_blank" class="attachment-link d-inline-flex align-items-center p-2 rounded bg-dark text-white text-decoration-none"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>${msg.attachment_url.split('/').pop()}</a></div>`;
|
||
}
|
||
}
|
||
|
||
let embedHtml = '';
|
||
if (msg.metadata) {
|
||
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata;
|
||
embedHtml = `
|
||
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid var(--blurple); max-width: 520px;">
|
||
${meta.site_name ? `<div class="embed-site-name mb-1" style="font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; font-weight: bold;">${escapeHTML(meta.site_name)}</div>` : ''}
|
||
${meta.title ? `<a href="${meta.url}" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc;">${escapeHTML(meta.title)}</a>` : ''}
|
||
${meta.description ? `<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);">${escapeHTML(meta.description)}</div>` : ''}
|
||
${meta.image ? `<div class="embed-image"><img src="${meta.image}" class="rounded" style="max-width: 100%; max-height: 300px; object-fit: contain;"></div>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const isMe = msg.user_id == window.currentUserId || msg.username == window.currentUsername;
|
||
const hasManageRights = window.canManageChannels || window.isServerOwner || false;
|
||
|
||
const pinHtml = `
|
||
<span class="action-btn pin ${msg.is_pinned ? 'active' : ''}" title="${msg.is_pinned ? 'Unpin' : 'Pin'}" data-id="${msg.id}" data-pinned="${msg.is_pinned ? '1' : '0'}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
|
||
</span>
|
||
`;
|
||
|
||
const actionsHtml = (isMe || hasManageRights) ? `
|
||
<div class="message-actions-menu">
|
||
${pinHtml}
|
||
${isMe ? `
|
||
<span class="action-btn edit" title="Edit" data-id="${msg.id}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
|
||
</span>
|
||
<span class="action-btn delete" title="Delete" data-id="${msg.id}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
||
</span>
|
||
` : ''}
|
||
</div>
|
||
` : '';
|
||
|
||
const pinnedBadge = msg.is_pinned ? `
|
||
<span class="pinned-badge ms-2" title="Pinned Message">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path></svg>
|
||
Pinned
|
||
</span>
|
||
` : '';
|
||
|
||
const mentionRegex = new RegExp(`@${window.currentUsername}\\b`, 'g');
|
||
if (msg.content.match(mentionRegex)) {
|
||
div.classList.add('mentioned');
|
||
}
|
||
if (msg.is_pinned) div.classList.add('pinned');
|
||
|
||
const ytRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
|
||
const dmRegex = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/;
|
||
const vimeoRegex = /(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/;
|
||
|
||
const ytMatch = msg.content.match(ytRegex);
|
||
const dmMatch = msg.content.match(dmRegex);
|
||
const vimeoMatch = msg.content.match(vimeoRegex);
|
||
|
||
let videoHtml = '';
|
||
if (ytMatch && ytMatch[1]) {
|
||
videoHtml = `<div class="video-embed mt-2"><iframe width="100%" height="315" src="https://www.youtube.com/embed/${ytMatch[1]}?autoplay=0" frameborder="0" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="border-radius: 8px; max-width: 560px;"></iframe></div>`;
|
||
} else if (dmMatch && dmMatch[1]) {
|
||
videoHtml = `<div class="video-embed mt-2"><iframe width="100%" height="315" src="https://www.dailymotion.com/embed/video/${dmMatch[1]}?autoplay=0&queue-enable=0&mute=0" frameborder="0" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="border-radius: 8px; max-width: 560px;"></iframe></div>`;
|
||
} else if (vimeoMatch && vimeoMatch[1]) {
|
||
videoHtml = `<div class="video-embed mt-2"><iframe width="100%" height="315" src="https://player.vimeo.com/video/${vimeoMatch[1]}?autoplay=0" frameborder="0" allow="fullscreen; picture-in-picture" allowfullscreen style="border-radius: 8px; max-width: 560px;"></iframe></div>`;
|
||
}
|
||
|
||
const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
|
||
|
||
const isRoleIconUrl = msg.role_icon && (msg.role_icon.startsWith('http') || msg.role_icon.startsWith('/'));
|
||
const roleIcon = msg.role_icon ? (isRoleIconUrl ? `<img src="${msg.role_icon}" class="role-icon ms-1" style="width: 12px; height: 12px; vertical-align: middle; object-fit: contain;">` : `<span class="ms-1" style="font-size: 12px; vertical-align: middle;">${msg.role_icon}</span>`) : '';
|
||
|
||
div.innerHTML = `
|
||
<div class="message-avatar" style="${avatarStyle}"></div>
|
||
<div class="message-content">
|
||
<div class="message-header">
|
||
<span class="message-author" style="${authorStyle}">${escapeHTML(msg.username)}</span>
|
||
${roleIcon}
|
||
<span class="message-time">${msg.time}</span>
|
||
${pinnedBadge}
|
||
</div>
|
||
<div class="message-text">
|
||
${escapeHTML(msg.content).replace(/\n/g, '<br>').replace(mentionRegex, `<span class="mention">@${window.currentUsername}</span>`)}
|
||
${attachmentHtml}
|
||
${videoHtml}
|
||
${embedHtml}
|
||
</div>
|
||
<div class="message-reactions mt-1" data-message-id="${msg.id}">
|
||
<span class="add-reaction-btn" title="Add Reaction">+</span>
|
||
</div>
|
||
</div>
|
||
${actionsHtml}
|
||
`;
|
||
messagesList.appendChild(div);
|
||
scrollToBottom(isMe);
|
||
|
||
// Ensure we scroll again when images/videos load
|
||
div.querySelectorAll('img, iframe').forEach(el => {
|
||
el.addEventListener('load', () => scrollToBottom(isMe));
|
||
});
|
||
}
|
||
});
|