38443-vm/assets/js/main.js
Flatlogic Bot 2bda3a08f3 role emote
2026-02-16 18:01:22 +00:00

2627 lines
132 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

document.addEventListener('DOMContentLoaded', () => {
const fileUpload = document.getElementById('file-upload');
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const messagesList = document.getElementById('messages-list');
const typingIndicator = document.getElementById('typing-indicator');
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 = 'rgba(255,255,255,0.05)';
div.style.minHeight = '70px';
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);
const originalBg = div.style.backgroundColor;
div.style.backgroundColor = 'var(--blurple)';
setTimeout(() => div.style.backgroundColor = originalBg, 200);
};
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 = 'rgba(255,255,255,0.05)';
div.textContent = emoji;
div.onclick = () => {
navigator.clipboard.writeText(emoji);
const originalBg = div.style.backgroundColor;
div.style.backgroundColor = 'var(--blurple)';
setTimeout(() => div.style.backgroundColor = originalBg, 200);
};
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 ? 'rgba(255,255,255,0.1)' : 'transparent';
btn.style.fontSize = '0.9em';
btn.innerHTML = `<span>${categoryIcons[cat] || 'โ“'}</span> <span>${cat}</span>`;
btn.onclick = () => {
sidebar.querySelectorAll('button').forEach(b => {
b.classList.remove('active');
b.style.backgroundColor = 'transparent';
});
btn.classList.add('active');
btn.style.backgroundColor = 'rgba(255,255,255,0.1)';
if (searchInput) searchInput.value = '';
renderGrid(cat);
};
sidebar.appendChild(btn);
});
if (uploadInput) {
uploadInput.onchange = async () => {
const file = uploadInput.files[0];
if (!file) return;
const name = prompt("Nom de l'emote:", file.name.split('.')[0]);
if (!name) return;
const fd = new FormData();
fd.append('emote', file);
fd.append('name', name);
const res = await (await fetch('api/emotes.php?action=upload', { method: 'POST', body: fd })).json();
if (res.success) {
renderGrid('Custom');
window.loadCustomEmotes();
} else alert(res.error);
uploadInput.value = '';
};
}
if (searchInput) {
searchInput.oninput = () => {
const term = searchInput.value.trim();
if (term) {
sidebar.querySelectorAll('button').forEach(b => {
b.classList.remove('active');
b.style.backgroundColor = 'transparent';
});
renderGrid(null, term);
} else {
const activeBtn = sidebar.querySelector('button.active');
renderGrid(activeBtn ? activeBtn.querySelector('span:last-child').textContent : 'Custom');
}
};
}
renderGrid('Custom');
}
// Call when tab is shown
const emotesTabBtn = document.getElementById('emotes-tab-btn');
if (emotesTabBtn) {
emotesTabBtn.addEventListener('shown.bs.tab', setupSettingsEmotes);
}
/**
* Centralized Emoji Picker Component
*/
const UniversalEmojiPicker = {
currentPicker: null,
async show(anchor, callback, options = {}) {
this.hide();
const picker = document.createElement('div');
picker.className = 'universal-emoji-picker p-0 overflow-hidden d-flex flex-column';
picker.style.width = options.width || '900px';
picker.style.height = options.height || '500px';
picker.style.backgroundColor = '#313338';
picker.style.border = '1px solid #1e1f22';
picker.style.borderRadius = '8px';
picker.style.boxShadow = '0 8px 24px rgba(0,0,0,0.5)';
picker.style.zIndex = '11000'; // Higher than most modals
picker.style.position = 'fixed';
picker.style.padding = '0'; // Explicitly override any CSS
// Tab Navigation
const tabs = document.createElement('div');
tabs.className = 'd-flex overflow-x-auto border-bottom border-secondary p-1 no-scrollbar';
tabs.style.gap = '2px';
tabs.style.backgroundColor = '#2b2d31';
tabs.style.flexShrink = '0';
tabs.style.minHeight = '42px';
// Search Container
const searchContainer = document.createElement('div');
searchContainer.className = 'p-2 border-bottom border-secondary';
searchContainer.style.backgroundColor = '#313338';
searchContainer.style.flexShrink = '0';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Chercher un emoji...';
searchInput.className = 'form-control form-control-sm bg-dark border-secondary text-white';
searchContainer.appendChild(searchInput);
// Grid Container
const grid = document.createElement('div');
grid.className = 'flex-grow-1 overflow-auto p-1 custom-scrollbar'; // Reduced padding
grid.style.display = 'grid';
grid.style.gridTemplateColumns = 'repeat(15, 1fr)';
grid.style.gap = '2px';
grid.style.backgroundColor = '#313338';
grid.style.alignContent = 'start';
const renderGrid = async (cat = null, term = '') => {
grid.innerHTML = '';
if (term) {
const customEmotes = window.CUSTOM_EMOTES_CACHE || [];
const filteredCustom = customEmotes.filter(e => e.name.toLowerCase().includes(term.toLowerCase()) || e.code.toLowerCase().includes(term.toLowerCase()));
const filteredStandard = ALL_EMOJIS.filter(e => e.includes(term));
filteredCustom.forEach(emote => {
const div = document.createElement('div');
div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-1';
div.style.cursor = 'pointer';
div.style.aspectRatio = '1/1';
div.innerHTML = `<img src="${emote.path}" style="width: 32px; height: 32px; object-fit: contain;">`;
div.title = emote.code;
div.onclick = (e) => {
e.stopPropagation();
callback(emote.code);
if (!options.keepOpen) this.hide();
};
grid.appendChild(div);
});
filteredStandard.forEach(emoji => {
const div = document.createElement('div');
div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-1';
div.style.cursor = 'pointer';
div.style.fontSize = '24px';
div.style.aspectRatio = '1/1';
div.textContent = emoji;
div.onclick = (e) => {
e.stopPropagation();
callback(emoji);
if (!options.keepOpen) this.hide();
};
grid.appendChild(div);
});
return;
}
if (cat === 'Custom') {
const customEmotes = (window.CUSTOM_EMOTES_CACHE && window.CUSTOM_EMOTES_CACHE.length > 0) ? window.CUSTOM_EMOTES_CACHE : await window.loadCustomEmotes();
if (customEmotes.length === 0) {
grid.innerHTML = '<div class="col-12 text-center text-muted p-4" style="grid-column: span 15; font-size: 0.8em;">Aucune emote personnalisรฉe.</div>';
return;
}
customEmotes.forEach(emote => {
const div = document.createElement('div');
div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-1';
div.style.cursor = 'pointer';
div.style.aspectRatio = '1/1';
div.innerHTML = `<img src="${emote.path}" style="width: 32px; height: 32px; object-fit: contain;">`;
div.title = emote.code;
div.onclick = (e) => {
e.stopPropagation();
callback(emote.code);
if (!options.keepOpen) this.hide();
};
grid.appendChild(div);
});
return;
}
let list = EMOJI_CATEGORIES[cat];
(list || []).forEach(emoji => {
const div = document.createElement('div');
div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-1';
div.style.cursor = 'pointer';
div.style.fontSize = '24px';
div.style.aspectRatio = '1/1';
div.textContent = emoji;
div.onclick = (e) => {
e.stopPropagation();
callback(emoji);
if (!options.keepOpen) this.hide();
};
grid.appendChild(div);
});
};
const cats = ['Custom', ...Object.keys(EMOJI_CATEGORIES)];
cats.forEach((cat, idx) => {
const btn = document.createElement('button');
btn.className = `btn btn-sm text-white border-0 p-2 ${idx === 0 ? 'active' : ''}`;
btn.style.backgroundColor = idx === 0 ? 'rgba(255,255,255,0.1)' : 'transparent';
btn.innerHTML = categoryIcons[cat] || 'โ“';
btn.title = cat;
btn.onclick = async () => {
tabs.querySelectorAll('button').forEach(b => {
b.classList.remove('active');
b.style.backgroundColor = 'transparent';
});
btn.classList.add('active');
btn.style.backgroundColor = 'rgba(255,255,255,0.1)';
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');
b.style.backgroundColor = 'transparent';
});
await renderGrid(null, term);
} else {
const activeBtn = tabs.querySelector('button.active');
const activeCat = activeBtn ? activeBtn.title : 'Custom';
await renderGrid(activeCat);
}
};
picker.appendChild(tabs);
picker.appendChild(searchContainer);
picker.appendChild(grid);
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;
// Ensure it doesn't go off screen at the bottom
if (top + picker.offsetHeight > window.innerHeight) {
top = window.innerHeight - picker.offsetHeight - 10;
}
// Ensure it doesn't go off screen at the top
if (top < 0) top = 10;
picker.style.top = `${top}px`;
picker.style.left = `${left}px`;
await renderGrid('Custom');
// Handle outside click
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();
}
}, { 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();
// 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;
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) {
if (!typingIndicator) return;
typingIndicator.textContent = `${username} is typing...`;
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
if (typingIndicator) 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;
}
});
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';
}
}
// 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)}
${renderRoleIconJS(msg.role_icon, '12px')}
<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;
const roleIds = (memberItem.dataset.roleIds || '').split(',').filter(id => id);
// Create or show member menu
document.querySelector('.member-context-menu')?.remove();
const menu = document.createElement('div');
menu.className = 'member-context-menu bg-dark border border-secondary rounded p-2';
menu.style.position = 'fixed';
menu.style.zIndex = '1000';
menu.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)';
menu.style.minWidth = '180px';
const rect = memberItem.getBoundingClientRect();
menu.style.top = `${rect.top}px`;
menu.style.left = `${rect.left - 190}px`;
let rolesHtml = '';
if (roleIds.length > 0) {
// Deduplicate and filter valid roles from serverRoles
const uniqueRoleIds = [...new Set(roleIds)];
const roles = uniqueRoleIds.map(id => serverRoles.find(r => r.id == id)).filter(r => r);
if (roles.length > 0) {
rolesHtml = `
<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: 12px; height: 12px; 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>
${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 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 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);
});
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 roles...</span></li>';
try {
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
const data = await resp.json();
if (data.success) {
addPermRoleList.innerHTML = '';
// Filter out roles already in overrides
const existingRoleIds = channelPermissionsData.map(p => parseInt(p.role_id));
const availableRoles = data.roles.filter(role => !existingRoleIds.includes(parseInt(role.id)));
if (availableRoles.length === 0) {
addPermRoleList.innerHTML = '<li><span class="dropdown-item disabled text-muted">No more roles to add</span></li>';
if (window.canManageServer) {
const divider = document.createElement('li');
divider.innerHTML = '<hr class="dropdown-divider border-secondary opacity-25">';
addPermRoleList.appendChild(divider);
const createLink = document.createElement('li');
createLink.innerHTML = '<a class="dropdown-item small text-info" href="#" data-bs-toggle="modal" data-bs-target="#serverSettingsModal" style="font-size: 0.8em;"><i class="fa-solid fa-gear me-1"></i> Create roles in Server Settings</a>';
addPermRoleList.appendChild(createLink);
}
return;
}
// Add Roles section
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: 12px; height: 12px; border-radius: 50%; background-color: ${role.color || '#99aab5'}; border: 1px solid rgba(255,255,255,0.1);"></div>
<span style="color: #dbdee1; 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);
selectOverrideRole(role.id, role.name);
} else {
alert("Error adding permission: " + (postData.error || "Unknown error"));
}
};
addPermRoleList.appendChild(li);
});
} else {
addPermRoleList.innerHTML = `<li><span class="dropdown-item text-danger">Error: ${data.error || 'Failed to load'}</span></li>`;
}
} catch (err) {
addPermRoleList.innerHTML = '<li><span class="dropdown-item text-danger">Network error</span></li>';
console.error(err);
}
}
function renderRoleOverridesList(channelId) {
channelPermissionsRolesList.innerHTML = '';
if (channelPermissionsData.length === 0) {
channelPermissionsRolesList.innerHTML = '<div class="text-center p-3 small" style="color: #dbdee1;">No overrides configured for this channel.</div>';
return;
}
// Sort: @everyone always at top, then by name
const sortedData = [...channelPermissionsData].sort((a, b) => {
const nameA = (a.role_name || '').toLowerCase();
const nameB = (b.role_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');
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 ${currentSelectedOverrideRole == p.role_id ? 'active' : ''}`;
item.style.cursor = 'pointer';
item.innerHTML = `
<div style="width: 8px; height: 8px; border-radius: 50%; background-color: ${p.role_color || '#99aab5'}; margin-right: 8px; flex-shrink: 0;"></div>
<span class="flex-grow-1 text-truncate">${p.role_name || 'Unknown Role'}</span>
`;
item.onclick = () => selectOverrideRole(p.role_id, p.role_name || 'Unknown Role');
channelPermissionsRolesList.appendChild(item);
});
}
function selectOverrideRole(roleId, roleName) {
currentSelectedOverrideRole = roleId;
const channelId = document.getElementById('edit-channel-id').value;
// Update list active state
renderRoleOverridesList(channelId);
selectedPermRoleName.textContent = roleName;
noRoleSelectedView.classList.add('d-none');
channelPermissionsSettings.classList.remove('d-none');
// Load existing permissions for this role
const p = channelPermissionsData.find(perm => perm.role_id == roleId) || { allow_permissions: 0, deny_permissions: 0 };
// Update toggles (for now only bit 1: View Channel)
updateToggleUI(1, 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;
await fetch('api_v1_channel_permissions.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel_id: channelId, role_id: currentSelectedOverrideRole })
});
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 roleId = currentSelectedOverrideRole;
let p = channelPermissionsData.find(perm => perm.role_id == roleId);
if (!p) {
p = { role_id: roleId, allow_permissions: 0, deny_permissions: 0 };
}
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;
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 })
});
// 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();
}
});
// 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 = '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 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.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 = 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, '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 roleIconHtml = renderRoleIconJS(role.icon_url, '12px');
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, '12px');
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');
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
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 parseCustomEmotes(text) {
let parsed = escapeHTML(text);
(window.CUSTOM_EMOTES_CACHE || []).forEach(emote => {
const imgHtml = `<img src="${emote.path}" alt="${emote.name}" title="${emote.code}" style="width: 22px; height: 22px; vertical-align: middle; object-fit: contain;">`;
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;
// 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');
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, '12px')}
</span>
<span class="message-timestamp">${msg.timestamp || 'Just now'}</span>
${pinnedBadge}
</div>
<div class="message-text">${contentWithMentions.replace(/\n/g, '<br>')}</div>
${attachmentHtml}
${embedHtml}
<div class="message-reactions mt-1" data-message-id="${msg.id}"></div>
</div>
${actionsHtml}
<div class="message-reaction-picker-anchor"></div>
`;
messagesList.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 = '#2b2d31';
btn.style.border = '1px solid #4e5058';
}
// 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;
}
})
.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;
}
});
});