document.addEventListener('DOMContentLoaded', () => { const fileUpload = document.getElementById('file-upload'); const chatForm = document.getElementById('chat-form'); const chatInput = document.getElementById('chat-input'); const messagesList = document.getElementById('messages-list'); const typingIndicator = document.getElementById('typing-indicator'); function scrollToBottom(force = false) { if (!messagesList) return; // Smart scroll: only scroll if user is already at the bottom or if forced (e.g. sending a message) const threshold = 150; // pixels margin const isAtBottom = messagesList.scrollHeight - messagesList.scrollTop <= messagesList.clientHeight + threshold; if (force || isAtBottom) { messagesList.scrollTo({ top: messagesList.scrollHeight, behavior: 'smooth' }); // Backup for non-smooth support or rendering delays setTimeout(() => { if (force || messagesList.scrollHeight - messagesList.scrollTop <= messagesList.clientHeight + threshold + 200) { messagesList.scrollTop = messagesList.scrollHeight; } }, 100); } } // Unified Emoji Categories - Expanded for "Complete" feel const EMOJI_CATEGORIES = { 'Smileys': ['๐Ÿ˜€', '๐Ÿ˜ƒ', '๐Ÿ˜„', '๐Ÿ˜', '๐Ÿ˜†', '๐Ÿ˜…', '๐Ÿคฃ', '๐Ÿ˜‚', '๐Ÿ™‚', '๐Ÿ™ƒ', '๐Ÿ˜‰', '๐Ÿ˜Š', '๐Ÿ˜‡', '๐Ÿฅฐ', '๐Ÿ˜', '๐Ÿคฉ', '๐Ÿ˜˜', '๐Ÿ˜—', '๐Ÿ˜š', '๐Ÿ˜™', '๐Ÿ˜‹', '๐Ÿ˜›', '๐Ÿ˜œ', '๐Ÿคช', '๐Ÿ˜', '๐Ÿค‘', '๐Ÿค—', '๐Ÿคญ', '๐Ÿคซ', '๐Ÿค”', '๐Ÿค', '๐Ÿคจ', '๐Ÿ˜', '๐Ÿ˜‘', '๐Ÿ˜ถ', '๐Ÿ˜', '๐Ÿ˜’', '๐Ÿ™„', '๐Ÿ˜ฌ', '๐Ÿคฅ', '๐Ÿ˜Œ', '๐Ÿ˜”', '๐Ÿ˜ช', '๐Ÿคค', '๐Ÿ˜ด', '๐Ÿ˜ท', '๐Ÿค’', '๐Ÿค•', '๐Ÿคข', '๐Ÿคฎ', '๐Ÿคง', '๐Ÿฅต', '๐Ÿฅถ', '๐Ÿฅด', '๐Ÿ˜ต', '๐Ÿคฏ', '๐Ÿค ', '๐Ÿฅณ', '๐Ÿ˜Ž', '๐Ÿค“', '๐Ÿง', '๐Ÿ˜•', '๐Ÿ˜Ÿ', '๐Ÿ™', 'โ˜น๏ธ', '๐Ÿ˜ฎ', '๐Ÿ˜ฏ', '๐Ÿ˜ฒ', '๐Ÿ˜ณ', '๐Ÿฅบ', '๐Ÿ˜ฆ', '๐Ÿ˜ง', '๐Ÿ˜จ', '๐Ÿ˜ฐ', '๐Ÿ˜ฅ', '๐Ÿ˜ข', '๐Ÿ˜ญ', '๐Ÿ˜ฑ', '๐Ÿ˜–', '๐Ÿ˜ฃ', '๐Ÿ˜ž', '๐Ÿ˜“', '๐Ÿ˜ฉ', '๐Ÿ˜ซ', '๐Ÿฅฑ', '๐Ÿ˜ค', '๐Ÿ˜ก', '๐Ÿ˜ ', '๐Ÿคฌ', '๐Ÿ˜ˆ', '๐Ÿ‘ฟ', '๐Ÿ‘น', '๐Ÿ‘บ', '๐Ÿ’€', 'โ˜ ๏ธ', '๐Ÿ’ฉ', '๐Ÿคก', '๐Ÿ‘ป', '๐Ÿ‘ฝ', '๐Ÿ‘พ', '๐Ÿค–', '๐Ÿ˜บ', '๐Ÿ˜ธ', '๐Ÿ˜ป', '๐Ÿ˜ผ', '๐Ÿ˜ฝ', '๐Ÿ™€', '๐Ÿ˜ฟ', '๐Ÿ˜พ', '๐Ÿ™ˆ', '๐Ÿ™‰', '๐Ÿ™Š', '๐Ÿ’‹', '๐Ÿ’Œ', '๐Ÿ’˜', '๐Ÿ’', '๐Ÿ’–', '๐Ÿ’—', '๐Ÿ’“', '๐Ÿ’ž', '๐Ÿ’•', '๐Ÿ’Ÿ', 'โฃ๏ธ', '๐Ÿ’”', 'โค๏ธ', '๐Ÿงก', '๐Ÿ’›', '๐Ÿ’š', '๐Ÿ’™', '๐Ÿ’œ', '๐Ÿ–ค', '๐Ÿค', '๐ŸคŽ', '๐Ÿ’ฏ', '๐Ÿ’ข', '๐Ÿ’ฅ', '๐Ÿ’ซ', '๐Ÿ’ฆ', '๐Ÿ’จ', '๐Ÿ•ณ๏ธ', '๐Ÿ’ฃ', '๐Ÿ’ฌ', '๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ', '๐Ÿ—จ๏ธ', '๐Ÿ—ฏ๏ธ', '๐Ÿ’ญ', '๐Ÿ’ค', '๐Ÿช', '๐ŸŒ ', '๐ŸŽ‡', '๐ŸŽ†', '๐ŸŒ‡', '๐ŸŒ†', '๐Ÿ™๏ธ', '๐ŸŒƒ', '๐ŸŒŒ'], 'Gestures': ['๐Ÿ‘‹', '๐Ÿคš', '๐Ÿ–๏ธ', 'โœ‹', '๐Ÿ––', '๐Ÿ‘Œ', '๐Ÿค', 'โœŒ๏ธ', '๐Ÿคž', '๐ŸคŸ', '๐Ÿค˜', '๐Ÿค™', '๐Ÿ‘ˆ', '๐Ÿ‘‰', '๐Ÿ‘†', '๐Ÿ–•', '๐Ÿ‘‡', 'โ˜๏ธ', '๐Ÿ‘', '๐Ÿ‘Ž', 'โœŠ', '๐Ÿ‘Š', '๐Ÿค›', '๐Ÿคœ', '๐Ÿ‘', '๐Ÿ™Œ', '๐Ÿ‘', '๐Ÿคฒ', '๐Ÿค', '๐Ÿ™', 'โœ๏ธ', '๐Ÿ’…', '๐Ÿคณ', '๐Ÿ’ช', '๐Ÿฆพ', '๐Ÿฆต', '๐Ÿฆฟ', '๐Ÿฆถ', '๐Ÿ‘‚', '๐Ÿฆป', '๐Ÿ‘ƒ', '๐Ÿง ', '๐Ÿฆท', '๐Ÿฆด', '๐Ÿ‘€', '๐Ÿ‘๏ธ', '๐Ÿ‘…', '๐Ÿ‘„', '๐Ÿ––', '๐Ÿค˜', '๐Ÿค™', '๐Ÿ–๏ธ', '๐Ÿ–•', '๐Ÿ––', 'โœ๏ธ', '๐Ÿคณ', '๐Ÿ’ช', '๐Ÿฆพ'], 'People': ['๐Ÿ‘ถ', '๐Ÿง’', '๐Ÿ‘ฆ', '๐Ÿ‘ง', '๐Ÿง‘', '๐Ÿ‘ฑ', '๐Ÿ‘จ', '๐Ÿ‘ฉ', '๐Ÿง“', '๐Ÿ‘ด', '๐Ÿ‘ต', '๐Ÿ‘ฎ', '๐Ÿ•ต๏ธ', '๐Ÿ’‚', '๐Ÿ‘ท', '๐Ÿคด', '๐Ÿ‘ธ', '๐Ÿ‘ณ', '๐Ÿ‘ฒ', '๐Ÿง•', '๐Ÿคต', '๐Ÿ‘ฐ', '๐Ÿคฐ', '๐Ÿคฑ', '๐Ÿ‘ผ', '๐ŸŽ…', '๐Ÿคถ', '๐Ÿฆธ', '๐Ÿฆน', '๐Ÿง™', '๐Ÿงš', '๐Ÿง›', '๐Ÿงœ', '๐Ÿง', '๐Ÿงž', '๐ŸงŸ', '๐Ÿ’†', '๐Ÿ’‡', '๐Ÿšถ', '๐Ÿƒ', '๐Ÿ’ƒ', '๐Ÿ•บ', '๐Ÿ•ด๏ธ', '๐Ÿ‘ฏ', '๐Ÿง–', '๐Ÿง—', '๐Ÿคบ', '๐Ÿ‡', 'โ›ท๏ธ', '๐Ÿ‚', '๐ŸŒ๏ธ', '๐Ÿ„', '๐Ÿšฃ', '๐ŸŠ', 'โ›น๏ธ', '๐Ÿ‹๏ธ', '๐Ÿšด', '๐Ÿšต', '๐Ÿคธ', '๐Ÿคผ', '๐Ÿคฝ', '๐Ÿคพ', '๐Ÿคน', '๐Ÿง˜', '๐Ÿ›€', '๐Ÿ›Œ'], 'Animals': ['๐Ÿถ', '๐Ÿฑ', '๐Ÿญ', '๐Ÿน', '๐Ÿฐ', '๐ŸฆŠ', '๐Ÿป', '๐Ÿผ', '๐Ÿจ', '๐Ÿฏ', '๐Ÿฆ', '๐Ÿฎ', '๐Ÿท', '๐Ÿฝ', '๐Ÿธ', '๐Ÿต', '๐Ÿ™ˆ', '๐Ÿ™‰', '๐Ÿ™Š', '๐Ÿ’', '๐Ÿ”', '๐Ÿง', '๐Ÿฆ', '๐Ÿค', '๐Ÿฃ', '๐Ÿฅ', '๐Ÿฆ†', '๐Ÿฆ…', '๐Ÿฆ‰', '๐Ÿฆ‡', '๐Ÿบ', '๐Ÿ—', '๐Ÿด', '๐Ÿฆ„', '๐Ÿ', '๐Ÿ›', '๐Ÿฆ‹', '๐ŸŒ', '๐Ÿž', '๐Ÿœ', '๐ŸฆŸ', '๐Ÿฆ—', '๐Ÿ•ท๏ธ', '๐Ÿ•ธ๏ธ', '่ ', '๐Ÿข', '๐Ÿ', '๐ŸฆŽ', '๐Ÿฆ–', '๐Ÿฆ•', '๐Ÿ™', '๐Ÿฆ‘', '๐Ÿฆ', '๐Ÿฆž', '๐Ÿฆ€', '๐Ÿก', '๐Ÿ ', '๐ŸŸ', '๐Ÿฌ', '๐Ÿณ', '๐Ÿ‹', '๐Ÿฆˆ', '๐ŸŠ', '๐Ÿ…', '๐Ÿ†', '๐Ÿฆ“', '๐Ÿฆ', '๐Ÿฆง', '๐Ÿ˜', '๐Ÿฆ›', '๐Ÿฆ', '๐Ÿช', '๐Ÿซ', '๐Ÿฆ’', '๐Ÿฆ˜', '๐Ÿฆฌ', '๐Ÿƒ', '๐Ÿ‚', '๐Ÿ„', '๐ŸŽ', '๐Ÿ–', '๐Ÿ', '๐Ÿ‘', '๐Ÿ', '๐ŸฆŒ', '๐Ÿ•', '๐Ÿฉ', '๐Ÿฆฎ', '๐Ÿ•โ€๐Ÿฆบ', '๐Ÿˆ', '๐Ÿˆโ€โฌ›', '๐Ÿ“', '๐Ÿฆƒ', '๐Ÿฆš', '๐Ÿฆœ', '๐Ÿฆข', '๐Ÿฆฉ', '๐Ÿ•Š๏ธ', '๐Ÿ‡', '๐Ÿฆ', '๐Ÿฆจ', '๐Ÿฆก', '๐Ÿฆฆ', '๐Ÿฆฅ', '๐Ÿ', '๐Ÿ€', '๐Ÿฟ๏ธ', '๐Ÿฆ”', '๐Ÿพ', '๐Ÿ‰', '๐Ÿฒ', '๐ŸŒต', '๐ŸŽ„', '๐ŸŒฒ', '๐ŸŒณ', '๐ŸŒด', '๐ŸŒฑ', '๐ŸŒฟ', 'โ˜˜๏ธ', '๐Ÿ€', '๐ŸŽ', '๐ŸŽ‹', '๐Ÿƒ', '๐Ÿ‚', '๐Ÿ', '๐Ÿ„', '๐Ÿš', '๐ŸŒพ'], 'Nature': ['๐Ÿ’', '๐ŸŒท', '๐ŸŒน', '๐Ÿฅ€', '๐ŸŒบ', '๐ŸŒธ', '๐ŸŒผ', '๐ŸŒป', '๐ŸŒž', '๐ŸŒ', '๐ŸŒ›', '๐ŸŒœ', '๐ŸŒš', '๐ŸŒ•', '๐ŸŒ–', '๐ŸŒ—', '๐ŸŒ˜', '๐ŸŒ‘', '๐ŸŒ’', '๐ŸŒ“', '๐ŸŒ”', '๐ŸŒ™', '๐ŸŒŽ', '๐ŸŒ', '๐ŸŒ', '๐Ÿช', '๐Ÿ’ซ', 'โญ๏ธ', '๐ŸŒŸ', 'โœจ', 'โšก๏ธ', 'โ˜„๏ธ', '๐Ÿ’ฅ', '๐Ÿ”ฅ', '๐ŸŒช๏ธ', '๐ŸŒˆ', 'โ˜€๏ธ', '๐ŸŒค๏ธ', 'โ›…๏ธ', '๐ŸŒฅ๏ธ', 'โ˜๏ธ', '๐ŸŒฆ๏ธ', '๐ŸŒง๏ธ', '๐ŸŒจ๏ธ', '๐ŸŒฉ๏ธ', 'โ„๏ธ', 'โ˜ƒ๏ธ', 'โ›„๏ธ', '๐ŸŒฌ๏ธ', '๐Ÿ’จ', '๐Ÿ’ง', '๐Ÿ’ฆ', 'โ˜”๏ธ', 'โ˜‚๏ธ', '๐ŸŒŠ', '๐ŸŒซ๏ธ', 'โ›ฐ๏ธ', '๐Ÿ”๏ธ', '๐Ÿ—ป', '๐ŸŒ‹', '๐Ÿœ๏ธ', '๐Ÿ–๏ธ', '๐Ÿ๏ธ', '๐Ÿ•๏ธ', 'โ›บ๏ธ'], 'Food': ['๐Ÿ', '๐ŸŽ', '๐Ÿ', '๐ŸŠ', '๐Ÿ‹', '๐ŸŒ', '๐Ÿ‰', '๐Ÿ‡', '๐Ÿ“', '๐Ÿˆ', '๐Ÿ’', '๐Ÿ‘', '๐Ÿฅญ', '๐Ÿ', '๐Ÿฅฅ', '๐Ÿฅ', '๐Ÿ…', '๐Ÿ†', '๐Ÿฅ‘', '๐Ÿฅฆ', '๐Ÿฅฌ', '๐Ÿฅ’', '๐ŸŒฝ', '๐Ÿฅ•', '๐Ÿง„', '๐Ÿง…', '๐Ÿ„', '๐Ÿฅœ', '๐ŸŒฐ', '๐Ÿž', '๐Ÿฅ', '๐Ÿฅ–', '๐Ÿฅจ', '๐Ÿฅฏ', '๐Ÿฅž', '๐Ÿง‡', '๐Ÿง€', '๐Ÿ–', '๐Ÿ—', '๐Ÿฅฉ', '๐Ÿฅ“', '๐Ÿ”', '๐ŸŸ', '๐Ÿ•', '๐ŸŒญ', '๐Ÿฅช', '๐ŸŒฎ', '๐ŸŒฏ', '๐Ÿฅ™', '๐Ÿง†', '๐Ÿณ', '๐Ÿฅ˜', '๐Ÿฒ', '๐Ÿฅฃ', '๐Ÿฅ—', '๐Ÿฟ', 'ใƒใ‚ฟใƒผ', '๐Ÿง‚', '๐Ÿฅซ', '๐Ÿฑ', '๐Ÿ˜', '๐Ÿ™', '๐Ÿš', '๐Ÿ›', '๐Ÿœ', '๐Ÿ', '๐Ÿ ', '๐Ÿข', '๐Ÿฃ', '๐Ÿค', '๐Ÿฅ', '๐Ÿฅฎ', '๐Ÿก', '๐ŸฅŸ', '๐Ÿฅ ', '๐Ÿฅก', '๐Ÿฆ', '๐Ÿง', '๐Ÿจ', '๐Ÿฉ', '๐Ÿช', '๐ŸŽ‚', '๐Ÿฐ', '๐Ÿง', '๐Ÿฅง', '๐Ÿซ', '๐Ÿฌ', '๐Ÿญ', '๐Ÿฎ', '๐Ÿฏ', '๐Ÿผ', '๐Ÿฅ›', 'โ˜•๏ธ', '๐Ÿต', '๐Ÿง‰', '๐Ÿฅค', '๐Ÿงƒ', '๐Ÿบ', '๐Ÿป', '๐Ÿฅ‚', '๐Ÿท', '๐Ÿฅƒ', '๐Ÿธ', '๐Ÿน', '๐Ÿพ', '๐ŸงŠ', '๐Ÿฅ„', '๐Ÿด', '๐Ÿฝ๏ธ'], 'Activities': ['โšฝ๏ธ', '๐Ÿ€', '๐Ÿˆ', 'โšพ๏ธ', '๐ŸฅŽ', '๐ŸŽพ', '๐Ÿ', '๐Ÿ‰', '๐ŸŽฑ', '๐Ÿ“', '๐Ÿธ', '๐Ÿฅ…', '๐Ÿ’', '๐Ÿ‘', '๐Ÿ', 'โ›ณ๏ธ', '๐Ÿน', '๐ŸŽฃ', '๐ŸฅŠ', '๐Ÿฅ‹', '๐Ÿ›น', '๐Ÿ›ท', 'โ›ธ๏ธ', '๐ŸฅŒ', '๐ŸŽฟ', 'โ›ท๏ธ', '๐Ÿ‚', '๐Ÿ‹๏ธ', '๐Ÿคบ', '๐Ÿคผ', '๐Ÿคธ', 'โ›น๏ธ', '๐Ÿคฝ', '๐Ÿคพ', '๐Ÿคน', '๐Ÿง˜', '๐Ÿ‡', '๐Ÿšฃ', '๐ŸŠ', '๐Ÿšด', '๐Ÿšต', '๐Ÿง—', '๐ŸŽ–๏ธ', '๐Ÿ†', '๐Ÿ…', '๐Ÿฅ‡', '๐Ÿฅˆ', '๐Ÿฅ‰', '๐ŸŽซ', '๐ŸŽŸ๏ธ', '๐ŸŽญ', '๐ŸŽจ', '๐ŸŽฌ', '๐ŸŽค', '๐ŸŽง', '๐ŸŽผ', '๐ŸŽน', '๐Ÿฅ', '๐ŸŽท', '๐ŸŽบ', '๐ŸŽธ', '๐Ÿช•', '๐ŸŽป', '๐ŸŽฒ', 'โ™Ÿ๏ธ', '๐ŸŽฏ', 'ใ‚ณใƒ„', '๐ŸŽฎ', '๐ŸŽฐ', '๐Ÿงฉ'], 'Travel': ['๐Ÿš—', '๐Ÿš•', '๐Ÿš™', '๐ŸšŒ', '๐ŸšŽ', '๐ŸŽ๏ธ', '๐Ÿš“', '๐Ÿš‘', '๐Ÿš’', '๐Ÿš', '๐Ÿšš', '๐Ÿš›', '๐Ÿšœ', '๐Ÿ›ต', '๐Ÿšฒ', '๐Ÿ›ด', '๐Ÿš', '๐Ÿ›ฃ๏ธ', '๐Ÿ›ค๏ธ', 'โ›ฝ๏ธ', '๐Ÿšจ', '๐Ÿšฅ', '๐Ÿšฆ', '๐Ÿšง', 'โš“๏ธ', 'โ›ต๏ธ', '๐Ÿšค', '๐Ÿ›ณ๏ธ', 'โ›ด๏ธ', '๐Ÿšข', 'โœˆ๏ธ', '๐Ÿ›ซ', '๐Ÿ›ฌ', '๐Ÿ’บ', '๐Ÿš', '๐ŸšŸ', 'ใ‚ฑใƒผใƒ–ใƒซ', '๐Ÿšก', '๐Ÿš€', '๐Ÿ›ธ', '๐Ÿ›ฐ๏ธ', 'โŒ›๏ธ', 'โณ', 'โŒš๏ธ', 'โฐ', 'โฑ๏ธ', 'โฒ๏ธ', '๐Ÿ•ฐ๏ธ', '๐ŸŒก๏ธ', '๐ŸŒƒ', '๐Ÿ™๏ธ', '๐ŸŒ„', '๐ŸŒ…', '๐ŸŒ†', '๐ŸŒ‡', '๐ŸŒ‰', '๐ŸŽ ', '๐ŸŽก', '๐ŸŽข', '๐Ÿš‚', '๐Ÿšƒ', '๐Ÿš„', '๐Ÿš…', '๐Ÿš†', '๐Ÿš‡', '๐Ÿšˆ', '๐Ÿš‰', '๐ŸšŠ', '๐Ÿš', '๐Ÿšž', '๐Ÿš‹'], 'Objects': ['โŒš๏ธ', '๐Ÿ“ฑ', '๐Ÿ“ฒ', '๐Ÿ’ป', 'โŒจ๏ธ', '๐Ÿ–ฑ๏ธ', '๐Ÿ–ฒ๏ธ', '๐Ÿ•น๏ธ', '๐Ÿ—œ๏ธ', '๐Ÿ’ฝ', '๐Ÿ’พ', '๐Ÿ’ฟ', '๐Ÿ“€', '๐Ÿ“ผ', '๐Ÿ“ท', '๐Ÿ“ธ', '๐Ÿ“น', '๐ŸŽฅ', '๐Ÿ“ฝ๏ธ', '๐ŸŽž๏ธ', '๐Ÿ“ž', '๐Ÿ“ ', '๐Ÿ“บ', '๐Ÿ“ป', '๐ŸŽ™๏ธ', '๐ŸŽš๏ธ', '๐ŸŽ›๏ธ', '๐Ÿงญ', 'โฑ๏ธ', 'โฒ๏ธ', 'โฐ', '๐Ÿ•ฐ๏ธ', 'โŒ›๏ธ', 'โณ', '๐Ÿ“ก', '๐Ÿ”‹', 'ใƒ—ใƒฉใ‚ฐ', '๐Ÿ’ก', '๐Ÿ”ฆ', '๐Ÿ•ฏ๏ธ', '๐Ÿช”', '๐Ÿงฏ', '๐Ÿ›ข๏ธ', '๐Ÿ’ธ', '๐Ÿ’ต', '๐Ÿ’ด', '๐Ÿ’ถ', '๐Ÿ’ท', '๐Ÿ’ฐ', '๐Ÿ’ณ', '๐Ÿ’Ž', 'โš–๏ธ', '๐Ÿงฐ', 'ใƒฌใƒณใƒ', '๐Ÿ”จ', 'โš’๏ธ', '๐Ÿ› ๏ธ', 'โ›๏ธ', 'ใƒŠใƒƒใƒˆ', 'โš™๏ธ', '๐Ÿงฑ', '้Ž–', '๐Ÿงฒ', '๐Ÿ”ซ', '๐Ÿ’ฃ', '๐Ÿงจ', '๐Ÿช“', 'ใƒŠใ‚คใƒ•', '๐Ÿ—ก๏ธ', 'โš”๏ธ', '็›พ', '๐Ÿšฌ', 'โšฐ๏ธ', 'โšฑ๏ธ', '๐Ÿบ', 'ๆฐดๆ™ถ', '๐Ÿงฟ', '๐Ÿ“ฟ', '๐Ÿ’ˆ', 'โš—๏ธ', 'ๆœ›้ ้ก', '๐Ÿ”ฌ', '๐Ÿ•ณ๏ธ', '๐Ÿ’Š', '๐Ÿ’‰', '๐Ÿฉธ', 'DNA', '๐Ÿฆ ', '๐Ÿงซ', '๐Ÿงช', '๐ŸŒก๏ธ', '๐Ÿงน', 'ใ‚ซใ‚ด', '๐Ÿงป', '็Ÿณ้นธ', 'ใ‚นใƒใƒณใ‚ธ', '๐Ÿช’', 'ใƒญใƒผใ‚ทใƒงใƒณ', '๐Ÿ›Ž๏ธ', '้ต', '๐Ÿ—๏ธ', 'ใƒ‰ใ‚ข', 'ๆค…ๅญ', 'ใ‚ฝใƒ•ใ‚ก', 'ใƒ™ใƒƒใƒ‰', '๐Ÿ›Œ', 'ใƒ†ใƒ‡ใ‚ฃใƒ™ใ‚ข', '้ก็ธ', '่ข‹', 'ใ‚ซใƒผใƒˆ', '๐ŸŽ', '๐ŸŽˆ', '๐ŸŽ', 'ใƒชใƒœใƒณ', '๐ŸŽŠ', '๐ŸŽ‰', 'ไบบๅฝข', 'ๆ็ฏ', '๐ŸŽ', '๐Ÿงง', 'โœ‰๏ธ', '๐Ÿ“ฉ', '๐Ÿ“จ', '๐Ÿ“ง', '๐Ÿ’Œ', '๐Ÿ“ฅ', '๐Ÿ“ค', '๐Ÿ“ฆ', '๐Ÿท๏ธ', 'ใƒ•ใ‚ฉใƒซใƒ€', '๐Ÿ“‚', 'ใ‚ซใƒฌใƒณใƒ€ใƒผ', '๐Ÿ“†', '๐Ÿ—’๏ธ', '๐Ÿ—“๏ธ', '๐Ÿ“‡', 'ใƒใƒฃใƒผใƒˆ', '๐Ÿ“‰', '๐Ÿ“Š', 'ใ‚ฏใƒชใƒƒใƒ—ใƒœใƒผใƒ‰', '็”ป้‹ฒ', '๐Ÿ“', '๐Ÿ“Ž', '๐Ÿ–‡๏ธ', 'ๅฎš่ฆ', '๐Ÿ“', 'ใƒใ‚ตใƒŸ', '๐Ÿ—ƒ๏ธ', 'ใ‚ญใƒฃใƒ“ใƒใƒƒใƒˆ', 'ใ‚ดใƒŸ็ฎฑ', '๐Ÿ”’', '๐Ÿ”“', '๐Ÿ”', '๐Ÿ”', '้ต', '๐Ÿ—๏ธ'], 'Symbols': ['๐Ÿ’˜', '๐Ÿ’', '๐Ÿ’–', '๐Ÿ’—', '๐Ÿ’“', '๐Ÿ’ž', '๐Ÿ’•', '๐Ÿ’Ÿ', 'โฃ๏ธ', '๐Ÿ’”', 'โค๏ธ', '๐Ÿงก', '๐Ÿ’›', '๐Ÿ’š', '๐Ÿ’™', '๐Ÿ’œ', '๐Ÿ–ค', '๐Ÿค', '๐ŸคŽ', '๐Ÿ’ฏ', '๐Ÿ’ข', '๐Ÿ’ฅ', '๐Ÿ’ซ', '๐Ÿ’ฆ', '๐Ÿ’จ', '๐Ÿ•ณ๏ธ', '๐Ÿ’ฃ', '๐Ÿ’ฌ', '๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ', '๐Ÿ—จ๏ธ', '๐Ÿ—ฏ๏ธ', '๐Ÿ’ญ', '๐Ÿ’ค', '๐ŸŒ', 'โ™ ๏ธ', 'โ™ฅ๏ธ', 'โ™ฆ๏ธ', 'โ™ฃ๏ธ', 'ใ‚ธใƒงใƒผใ‚ซใƒผ', '๐Ÿ€„๏ธ', '๐ŸŽด', '๐Ÿ”‡', '๐Ÿ”ˆ', '๐Ÿ”‰', '๐Ÿ”Š', '๐Ÿ“ข', '๐Ÿ“ฃ', '๐Ÿ“ฏ', '๐Ÿ””', '๐Ÿ”•', '๐ŸŽผ', '๐ŸŽต', '๐ŸŽถ', '๐Ÿ’น', 'ATM', '๐Ÿšฎ', '๐Ÿšฐ', 'โ™ฟ๏ธ', '๐Ÿšน', '๐Ÿšบ', '๐Ÿšป', '๐Ÿšผ', '๐Ÿšพ', '๐Ÿ›‚', 'ใ‚ซใ‚นใ‚ฟใƒ ', 'ใƒใ‚ฒใƒผใ‚ธ', '๐Ÿ›…', 'โš ๏ธ', '๐Ÿšธ', 'โ›”๏ธ', '๐Ÿšซ', '๐Ÿšณ', '๐Ÿšฏ', '๐Ÿšฑ', '๐Ÿšท', '๐Ÿ“ต', '๐Ÿ”ž', 'ๆ”พๅฐ„่ƒฝ', 'ใƒใ‚คใ‚ช', 'โฌ†๏ธ', 'โ†—๏ธ', 'โžก๏ธ', 'โ†˜๏ธ', 'โฌ‡๏ธ', 'โ†™๏ธ', 'โฌ…๏ธ', 'โ†–๏ธ', 'โ†•๏ธ', 'โ†”๏ธ', 'โ†ฉ๏ธ', 'โ†ช๏ธ', 'โคด๏ธ', 'โคต๏ธ', '๐Ÿ”ƒ', '๐Ÿ”„', '๐Ÿ”™', '๐Ÿ”š', '๐Ÿ”›', '๐Ÿ”œ', '๐Ÿ”', '๐Ÿ›', 'โš›๏ธ', '๐Ÿ•‰๏ธ', 'โœก๏ธ', 'โ˜ธ๏ธ', 'โ˜ฏ๏ธ', 'โœ๏ธ', 'โ˜ฆ๏ธ', 'โ˜ช๏ธ', 'โ˜ฎ๏ธ', '๐Ÿ•Ž', '๐Ÿ”ฏ', 'โ™ˆ๏ธ', 'โ™‰๏ธ', 'โ™Š๏ธ', 'โ™‹๏ธ', 'โ™Œ๏ธ', 'โ™๏ธ', 'โ™Ž๏ธ', 'โ™๏ธ', 'โ™๏ธ', 'โ™‘๏ธ', 'โ™’๏ธ', 'โ™“๏ธ', 'โ›Ž', '๐Ÿ”€', '๐Ÿ”', '๐Ÿ”‚', 'โ–ถ๏ธ', 'โฉ', 'โญ๏ธ', 'โฏ๏ธ', 'โ—€๏ธ', 'โช', 'โฎ๏ธ', '๐Ÿ”ผ', 'โซ', '๐Ÿ”ฝ', 'โฌ', 'โธ๏ธ', 'โน๏ธ', 'โบ๏ธ', 'โ๏ธ', '๐ŸŽฆ', '๐Ÿ”…', '๐Ÿ”†', '๐Ÿ“ถ', '๐Ÿ“ณ', '๐Ÿ“ด', 'โž•', 'โž–', 'โž—', 'โœ–๏ธ', 'โ™พ๏ธ', 'โ€ผ๏ธ', 'โ‰๏ธ', 'โ“', 'โ”', 'โ•', 'โ—๏ธ', 'ใ€ฐ๏ธ', '๐Ÿ’ฑ', '๐Ÿ’ฒ', 'โš•๏ธ', 'โ™ป๏ธ', 'โšœ๏ธ', '๐Ÿ”ฑ', '๐Ÿ“›', '๐Ÿ”ฐ', 'โญ•๏ธ', 'โœ…', 'โ˜‘๏ธ', 'โœ”๏ธ', 'โœ–๏ธ', 'โŒ', 'โŽ', 'โžฐ', 'โžฟ', 'ใ€ฝ๏ธ', 'โœณ๏ธ', 'โœด๏ธ', 'โ‡๏ธ', 'โ€ผ๏ธ', '๐Ÿˆ', '๐Ÿˆ‚๏ธ', '๐Ÿˆท๏ธ', '๐Ÿˆถ', '๐Ÿˆฏ๏ธ', '๐Ÿ‰', '๐Ÿˆน', '๐Ÿˆš๏ธ', '๐Ÿˆฒ', '๐Ÿ‰‘', '๐Ÿˆธ', '๐Ÿˆด', '๐Ÿˆณ', 'ใŠ—๏ธ', 'ใŠ™๏ธ', '๐Ÿˆบ', '๐Ÿˆต', '๐Ÿ”ด', '๐ŸŸ ', '๐ŸŸก', '๐ŸŸข', '๐Ÿ”ต', '๐ŸŸฃ', '๐ŸŸค', 'โšซ๏ธ', 'โšช๏ธ', '๐ŸŸฅ', '๐ŸŸง', '๐ŸŸจ', '๐ŸŸฉ', '๐ŸŸฆ', '๐ŸŸช', '๐ŸŸซ', 'โฌ›๏ธ', 'โฌœ๏ธ', 'โ™ˆ', 'โ™‰', 'โ™Š', 'โ™‹', 'โ™Œ', 'โ™', 'โ™Ž', 'โ™', 'โ™', 'โ™‘', 'โ™’', 'โ™“', 'โ›Ž', 'โ˜ธ', 'โ˜ฆ', 'โ˜ฆ', 'โ˜ช', 'โ˜ฎ', 'โ˜ฏ', 'โ™ˆ', 'โ™‰', 'โ™Š', 'โ™‹', 'โ™Œ', 'โ™', 'โ™Ž', 'โ™', 'โ™', 'โ™‘', 'โ™’', 'โ™“', 'โ›Ž', 'โ™€', 'โ™‚', 'โš•', 'โ™พ', 'โš“', 'โš”', 'โš–', 'โš—', 'โš™', 'โš–', 'โš“', 'โš”'], 'Flags': ['๐Ÿ', '๐Ÿšฉ', '๐ŸŽŒ', '๐Ÿด', '๐Ÿณ๏ธ', '๐Ÿณ๏ธโ€๐ŸŒˆ', '๐Ÿณ๏ธโ€โšง๏ธ', '๐Ÿดโ€โ˜ ๏ธ', '๐Ÿ‡ฆ๐Ÿ‡ซ', '๐Ÿ‡ฆ๐Ÿ‡ฝ', '๐Ÿ‡ฆ๐Ÿ‡ฑ', '๐Ÿ‡ฉ๐Ÿ‡ฟ', '๐Ÿ‡ฆ๐Ÿ‡ฒ', '๐Ÿ‡ฆ๐Ÿ‡บ', '๐Ÿ‡ฆ๐Ÿ‡น', '๐Ÿ‡ฆ๐Ÿ‡ฟ', '๐Ÿ‡ง๐Ÿ‡ช', '๐Ÿ‡ง๐Ÿ‡ท', '๐Ÿ‡จ๐Ÿ‡ฆ', '๐Ÿ‡จ๐Ÿ‡ฑ', '๐Ÿ‡จ๐Ÿ‡ณ', '๐Ÿ‡จ๐Ÿ‡ด', '๐Ÿ‡จ๐Ÿ‡ฟ', '๐Ÿ‡ฉ๐Ÿ‡ฐ', '๐Ÿ‡ช๐Ÿ‡ฌ', '๐Ÿ‡ซ๐Ÿ‡ฎ', '๐Ÿ‡ซ๐Ÿ‡ท', '๐Ÿ‡ฉ๐Ÿ‡ช', '๐Ÿ‡ฌ๐Ÿ‡ท', '๐Ÿ‡ญ๐Ÿ‡ฐ', '๐Ÿ‡ฎ๐Ÿ‡ณ', '๐Ÿ‡ฎ๐Ÿ‡ฉ', '๐Ÿ‡ฎ๐Ÿ‡ช', '๐Ÿ‡ฎ๐Ÿ‡ฑ', '๐Ÿ‡ฎ๐Ÿ‡น', '๐Ÿ‡ฏ๐Ÿ‡ต', '๐Ÿ‡ฐ๐Ÿ‡ท', '๐Ÿ‡ฒ๐Ÿ‡ฝ', '๐Ÿ‡ณ๐Ÿ‡ฑ', '๐Ÿ‡ณ๐Ÿ‡ฟ', '๐Ÿ‡ณ๐Ÿ‡ด', '๐Ÿ‡ต๐Ÿ‡ฐ', '๐Ÿ‡ต๐Ÿ‡ญ', '๐Ÿ‡ต๐Ÿ‡ฑ', '๐Ÿ‡ต๐Ÿ‡น', '๐Ÿ‡ท๐Ÿ‡บ', '๐Ÿ‡ธ๐Ÿ‡ฆ', '๐Ÿ‡ธ๐Ÿ‡ฌ', '๐Ÿ‡ฟ๐Ÿ‡ฆ', '๐Ÿ‡ช๐Ÿ‡ธ', '๐Ÿ‡ธ๐Ÿ‡ช', '๐Ÿ‡จ๐Ÿ‡ญ', '๐Ÿ‡น๐Ÿ‡ญ', '๐Ÿ‡น๐Ÿ‡ท', '๐Ÿ‡บ๐Ÿ‡ฆ', '๐Ÿ‡ฆ๐Ÿ‡ช', '๐Ÿ‡ฌ๐Ÿ‡ง', '๐Ÿ‡บ๐Ÿ‡ธ', '๐Ÿ‡ป๐Ÿ‡ณ', '๐Ÿ‡ฆ๐Ÿ‡ท', '๐Ÿ‡ง๐Ÿ‡ฉ', '๐Ÿ‡ง๐Ÿ‡ช', '๐Ÿ‡ง๐Ÿ‡ด', '๐Ÿ‡ฎ๐Ÿ‡ฉ', '๐Ÿ‡ฎ๐Ÿ‡ท', '๐Ÿ‡ฎ๐Ÿ‡ถ', '๐Ÿ‡ฏ๐Ÿ‡ฒ', '๐Ÿ‡ฐ๐Ÿ‡ฟ', '๐Ÿ‡ฐ๐Ÿ‡ช', '๐Ÿ‡ฒ๐Ÿ‡พ', '๐Ÿ‡ฒ๐Ÿ‡ฆ', '๐Ÿ‡ณ๐Ÿ‡ฌ', '๐Ÿ‡ต๐Ÿ‡ช', '๐Ÿ‡ท๐Ÿ‡ด', '๐Ÿ‡ท๐Ÿ‡ธ', '๐Ÿ‡ธ๐Ÿ‡ฐ', '๐Ÿ‡บ๐Ÿ‡พ', '๐Ÿ‡ฟ๐Ÿ‡ผ'] }; const categoryIcons = { 'Custom': 'โญ', 'Smileys': '๐Ÿ˜€', 'Gestures': '๐Ÿ‘Œ', 'People': '๐Ÿ‘ถ', 'Animals': '๐Ÿถ', 'Nature': '๐ŸŒต', 'Food': '๐Ÿ', 'Activities': 'โšฝ๏ธ', 'Travel': '๐Ÿš—', 'Objects': 'โŒš๏ธ', 'Symbols': 'โค๏ธ', 'Flags': '๐Ÿ' }; const ALL_EMOJIS = Object.values(EMOJI_CATEGORIES).flat(); // Unified custom emote loading and caching window.CUSTOM_EMOTES_CACHE = []; window.loadCustomEmotes = async () => { try { const resp = await fetch('api/emotes.php?action=list'); const data = await resp.json(); if (data.success) { window.CUSTOM_EMOTES_CACHE = data.emotes || []; return window.CUSTOM_EMOTES_CACHE; } return []; } catch (e) { console.error("Failed to load custom emotes", e); return []; } }; // Settings Emotes Tab Logic async function setupSettingsEmotes() { console.log("Setting up Emotes Tab..."); const sidebar = document.getElementById('settings-emotes-sidebar'); const grid = document.getElementById('settings-emotes-grid'); const searchInput = document.getElementById('settings-emotes-search'); const uploadZone = document.getElementById('custom-emote-upload-zone'); const uploadInput = document.getElementById('emote-upload-input'); if (!sidebar || !grid) return; const categories = ['Custom', ...Object.keys(EMOJI_CATEGORIES)]; const renderGrid = async (category, searchTerm = '') => { grid.innerHTML = '
'; if (category === 'Custom' && !searchTerm) { if (uploadZone) uploadZone.classList.remove('d-none'); const emotes = await window.loadCustomEmotes(); grid.innerHTML = ''; if (emotes.length === 0) { grid.innerHTML = '
Aucune emote personnalisรฉe. Ajoutez-en une !
'; } else { emotes.forEach(emote => { const div = document.createElement('div'); div.className = 'role-emoji-item rounded d-flex flex-column align-items-center justify-content-center p-2 text-center position-relative'; div.style.cursor = 'pointer'; div.style.backgroundColor = 'var(--separator-soft)'; div.style.height = 'auto'; div.innerHTML = ` ${emote.code}
`; div.onmouseenter = () => div.querySelector('.emote-actions')?.classList.remove('d-none'); div.onmouseleave = () => div.querySelector('.emote-actions')?.classList.add('d-none'); div.onclick = (e) => { if (e.target.closest('.emote-actions')) return; navigator.clipboard.writeText(emote.code); }; div.querySelector('.delete-emote').onclick = async (e) => { e.stopPropagation(); if (!confirm(`Supprimer l'emote ${emote.code} ?`)) return; const fd = new FormData(); fd.append('id', emote.id); const res = await (await fetch('api/emotes.php?action=delete', { method: 'POST', body: fd })).json(); if (res.success) renderGrid('Custom'); }; div.querySelector('.edit-emote').onclick = async (e) => { e.stopPropagation(); const newName = prompt("Nouveau nom (sans les :)", emote.name); if (!newName || newName === emote.name) return; const fd = new FormData(); fd.append('id', emote.id); fd.append('name', newName); const res = await (await fetch('api/emotes.php?action=rename', { method: 'POST', body: fd })).json(); if (res.success) renderGrid('Custom'); }; grid.appendChild(div); }); } } else { if (uploadZone) uploadZone.classList.add('d-none'); grid.innerHTML = ''; const list = searchTerm ? ALL_EMOJIS.filter(e => e.includes(searchTerm)) : EMOJI_CATEGORIES[category]; (list || []).forEach(emoji => { const div = document.createElement('div'); div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-2'; div.style.cursor = 'pointer'; div.style.fontSize = '24px'; div.style.backgroundColor = 'var(--separator-soft)'; div.textContent = emoji; div.onclick = () => { navigator.clipboard.writeText(emoji); }; grid.appendChild(div); }); } }; sidebar.innerHTML = ''; categories.forEach((cat, idx) => { const btn = document.createElement('button'); btn.className = `btn w-100 text-start text-white border-0 py-2 px-3 mb-1 d-flex align-items-center gap-2 ${idx === 0 ? 'active' : ''}`; btn.style.backgroundColor = idx === 0 ? 'var(--separator)' : 'transparent'; btn.innerHTML = ` ${cat}`; btn.onclick = async () => { sidebar.querySelectorAll('button').forEach(b => { b.classList.remove('active'); b.style.backgroundColor = 'transparent'; }); btn.classList.add('active'); btn.style.backgroundColor = 'var(--separator)'; await renderGrid(cat); }; sidebar.appendChild(btn); }); searchInput.oninput = async () => { const term = searchInput.value.trim(); if (term) { sidebar.querySelectorAll('button').forEach(b => { b.classList.remove('active'); b.style.backgroundColor = 'transparent'; }); await renderGrid(null, term); } else { const activeBtn = sidebar.querySelector('button.active'); const activeCat = activeBtn ? activeBtn.innerText.trim() : 'Custom'; await renderGrid(activeCat); } }; if (uploadInput) { uploadInput.onchange = async () => { const file = uploadInput.files[0]; if (!file) return; const fd = new FormData(); fd.append('emote', file); const res = await (await fetch('api/emotes.php?action=upload', { method: 'POST', body: fd })).json(); if (res.success) renderGrid('Custom'); else alert(res.error || "Upload failed"); }; } await renderGrid('Custom'); } const UniversalEmojiPicker = { currentPicker: null, show: async function(anchor, callback, options = {}) { this.hide(); const picker = document.createElement('div'); picker.className = 'emoji-picker-container rounded shadow-lg p-0 d-flex flex-column'; picker.style.position = 'fixed'; picker.style.zIndex = '10000'; picker.style.width = options.width || '400px'; picker.style.height = options.height || '450px'; picker.style.backgroundColor = '#2b2d31'; picker.style.border = '1px solid #1e1f22'; picker.style.display = 'flex'; picker.style.flexDirection = 'column'; const mainLayout = document.createElement('div'); mainLayout.className = 'd-flex flex-grow-1 overflow-hidden'; const tabs = document.createElement('div'); tabs.className = 'emoji-sidebar d-flex flex-column p-2 border-end border-secondary custom-scrollbar'; tabs.style.width = '60px'; tabs.style.overflowY = 'auto'; tabs.style.backgroundColor = '#1e1f22'; const contentArea = document.createElement('div'); contentArea.className = 'd-flex flex-column flex-grow-1'; const searchContainer = document.createElement('div'); searchContainer.className = 'p-2 border-bottom border-secondary'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Search emojis...'; searchInput.className = 'form-control form-control-sm bg-dark border-secondary text-white'; searchContainer.appendChild(searchInput); const grid = document.createElement('div'); grid.className = 'emoji-grid flex-grow-1 p-2 overflow-auto custom-scrollbar'; grid.style.display = 'grid'; grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(36px, 1fr))'; grid.style.gap = '4px'; grid.style.alignContent = 'start'; const categories = ['Custom', ...Object.keys(EMOJI_CATEGORIES)]; const renderGrid = async (category, searchTerm = '') => { grid.innerHTML = ''; if (category === 'Custom' && !searchTerm) { const emotes = await window.loadCustomEmotes(); emotes.forEach(emote => { const div = document.createElement('div'); div.className = 'emoji-item rounded p-1 text-center'; div.style.cursor = 'pointer'; div.innerHTML = ``; div.onclick = () => { callback(emote.code); if (!options.keepOpen) this.hide(); }; grid.appendChild(div); }); } else { const list = searchTerm ? ALL_EMOJIS.filter(e => e.includes(searchTerm)) : EMOJI_CATEGORIES[category]; (list || []).forEach(emoji => { const div = document.createElement('div'); div.className = 'emoji-item rounded p-1 text-center'; div.style.cursor = 'pointer'; div.style.fontSize = '20px'; div.textContent = emoji; div.onclick = () => { callback(emoji); if (!options.keepOpen) this.hide(); }; grid.appendChild(div); }); } }; categories.forEach(cat => { const btn = document.createElement('button'); btn.className = 'btn btn-link text-white text-decoration-none p-2 mb-1 opacity-75'; btn.title = cat; btn.innerHTML = categoryIcons[cat] || '๐Ÿ˜€'; btn.onclick = async () => { tabs.querySelectorAll('button').forEach(b => b.classList.remove('active')); btn.classList.add('active'); await renderGrid(cat); }; tabs.appendChild(btn); }); searchInput.oninput = async () => { const term = searchInput.value.trim(); if (term) { tabs.querySelectorAll('button').forEach(b => b.classList.remove('active')); await renderGrid(null, term); } else { const activeBtn = tabs.querySelector('button.active'); await renderGrid(activeBtn ? activeBtn.title : 'Custom'); } }; mainLayout.appendChild(tabs); contentArea.appendChild(searchContainer); contentArea.appendChild(grid); mainLayout.appendChild(contentArea); picker.appendChild(mainLayout); document.body.appendChild(picker); this.currentPicker = picker; // Positioning const rect = anchor.getBoundingClientRect(); let top = rect.top - picker.offsetHeight - 10; if (top < 0) top = rect.bottom + 10; let left = rect.left; if (left + picker.offsetWidth > window.innerWidth) left = window.innerWidth - picker.offsetWidth - 20; if (left < 10) left = 10; picker.style.top = `${top}px`; picker.style.left = `${left}px`; await renderGrid('Custom'); const outsideClick = (e) => { if (!picker.contains(e.target) && e.target !== anchor && !anchor.contains(e.target)) { this.hide(); document.removeEventListener('click', outsideClick); } }; setTimeout(() => document.addEventListener('click', outsideClick), 10); }, hide() { if (this.currentPicker) { this.currentPicker.remove(); this.currentPicker = null; } } }; // Replace old showEmojiPicker and role grid logic window.showEmojiPicker = (anchor, callback) => UniversalEmojiPicker.show(anchor, callback, { width: '900px', height: '500px' }); window.renderEmojiToElement = (code, el) => { if (!el) return; if (!code) { el.innerHTML = ""; return; } if (typeof code === "string" && code.startsWith(':') && code.endsWith(':')) { const ce = window.CUSTOM_EMOTES_CACHE.find(e => e.code === code); if (ce) { el.innerHTML = ``; return; } } el.textContent = code; }; // Unified Emoji Picker & Modal Logic document.addEventListener("click", (e) => { // Emoji Picker Triggers const triggers = { "role-emoji-select-btn": { target: "edit-role-icon", preview: "selected-role-emoji-preview" }, "add-autorole-emoji-btn": { target: "add-autorole-icon", preview: "add-autorole-emoji-preview" }, "edit-autorole-emoji-btn": { target: "edit-autorole-icon", preview: "edit-autorole-emoji-preview" } }; const btn = e.target.closest("button[id]"); if (btn && triggers[btn.id]) { e.preventDefault(); const config = triggers[btn.id]; UniversalEmojiPicker.show(btn, (emoji) => { const input = document.getElementById(config.target); const preview = document.getElementById(config.preview); if (input) input.value = emoji; window.renderEmojiToElement(emoji, preview); }, { width: "900px", height: "500px" }); return; } // Chat Emoji Picker const chatEmojiBtn = e.target.closest("#chat-emoji-btn"); if (chatEmojiBtn) { e.preventDefault(); UniversalEmojiPicker.show(chatEmojiBtn, (emoji) => { const chatInput = document.getElementById("chat-input"); if (chatInput) { chatInput.value += emoji; chatInput.focus(); chatInput.dispatchEvent(new Event('input')); } }, { keepOpen: true, width: "900px", height: "500px" }); return; } // Autorole Edit modal filling const editAutoroleBtn = e.target.closest(".edit-autorole-btn"); if (editAutoroleBtn) { const id = editAutoroleBtn.dataset.id; const icon = editAutoroleBtn.dataset.icon; const title = editAutoroleBtn.dataset.title; const roleId = editAutoroleBtn.dataset.roleId; const idInput = document.getElementById("edit-autorole-id"); const iconInput = document.getElementById("edit-autorole-icon"); const titleInput = document.getElementById("edit-autorole-title"); const roleIdInput = document.getElementById("edit-autorole-role-id"); const preview = document.getElementById("edit-autorole-emoji-preview"); if (idInput) idInput.value = id; if (iconInput) iconInput.value = icon; if (titleInput) titleInput.value = title; if (roleIdInput) roleIdInput.value = roleId; if (preview) window.renderEmojiToElement(icon, preview); return; } }); window.loadCustomEmotes(); const emotesTabBtn = document.getElementById('emotes-tab-btn'); if (emotesTabBtn) { emotesTabBtn.addEventListener('click', () => { setupSettingsEmotes(); }); } // Scroll to bottom scrollToBottom(true); const currentChannel = window.activeChannelId || new URLSearchParams(window.location.search).get('channel_id') || 1; const currentThread = new URLSearchParams(window.location.search).get('thread_id'); let typingTimeout; // Notification Permission if ("Notification" in window && Notification.permission === "default") { Notification.requestPermission(); } // WebSocket for real-time let ws; let voiceHandler; if (typeof VoiceChannel !== 'undefined') { voiceHandler = new VoiceChannel(null, window.voiceSettings); window.voiceHandler = voiceHandler; console.log('VoiceHandler initialized'); // Start global voice sessions polling setInterval(() => { VoiceChannel.refreshAllVoiceUsers(); }, 3000); VoiceChannel.refreshAllVoiceUsers(); } function connectWS() { console.log('Connecting to WebSocket...'); try { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; // Use reverse proxy path /ws ws = new WebSocket(protocol + '//' + window.location.hostname + '/ws'); ws.onopen = () => { console.log('WebSocket connected'); if (voiceHandler) voiceHandler.ws = ws; ws.send(JSON.stringify({ type: 'presence', user_id: window.currentUserId, status: 'online' })); }; ws.onmessage = (e) => { const msg = JSON.parse(e.data); // Voice signaling if (msg.type && msg.type.startsWith('voice_')) { if (voiceHandler) voiceHandler.handleSignaling(msg); return; } if (msg.type === 'message') { const data = JSON.parse(msg.data); if (data.channel_id == currentChannel) { // For forums, only append if we are in the correct thread if (window.activeChannelType === 'forum') { if (!currentThread || data.thread_id != currentThread) { return; } } else if (data.thread_id) { // If it's not a forum channel but has a thread_id (shouldn't happen with current logic but for safety) return; } appendMessage(data); // Desktop Notifications for mentions if (data.content.includes(`@${window.currentUsername}`) && data.user_id != window.currentUserId) { if (Notification.permission === "granted" && !window.isDndMode) { new Notification(`Mention in #${window.currentChannelName}`, { body: `${data.username}: ${data.content}`, icon: data.avatar_url || '' }); } } } } else if (msg.type === 'typing') { if (msg.channel_id == currentChannel && msg.user_id != window.currentUserId) { showTyping(msg.username); } } else if (msg.type === 'reaction') { updateReactionUI(msg.message_id, msg.reactions); } else if (msg.type === 'message_edit') { const item = document.querySelector(`.message-item[data-id="${msg.message_id}"]`); if (item) { item.dataset.rawContent = msg.content; const el = item.querySelector('.message-text'); if (el) el.innerHTML = parseCustomEmotes(msg.content); } } else if (msg.type === 'message_delete') { document.querySelector(`.message-item[data-id="${msg.message_id}"]`)?.remove(); } else if (msg.type === 'presence') { updatePresenceUI(msg.user_id, msg.status); } }; ws.onclose = () => { console.log('WebSocket connection closed. Reconnecting...'); setTimeout(connectWS, 3000); }; } catch (e) { console.warn('WebSocket connection failed:', e); } } connectWS(); // Polling as fallback for real-time let lastMessageId = 0; const findLastMessageId = () => { const items = document.querySelectorAll('.message-item'); if (items.length > 0) { lastMessageId = Math.max(...Array.from(items).map(i => parseInt(i.dataset.id) || 0)); } }; findLastMessageId(); setInterval(async () => { if (!currentChannel) return; // For forums, if we're not in a thread, don't poll for messages if (window.activeChannelType === 'forum' && !currentThread) return; // If we are in a non-forum channel, we should NOT have a currentThread if (window.activeChannelType !== 'forum' && currentThread) return; try { const threadParam = currentThread ? `&thread_id=${currentThread}` : ''; const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&after_id=${lastMessageId}${threadParam}`); const data = await resp.json(); if (data.success && data.messages && data.messages.length > 0) { data.messages.forEach(msg => { // Double check thread_id in JS side too if (window.activeChannelType === 'forum') { if (msg.thread_id != currentThread) return; } else { if (msg.thread_id) return; } appendMessage(msg); }); } } catch (e) { } }, 1000); function showTyping(username) { if (!typingIndicator) return; typingIndicator.textContent = `${username} is typing...`; clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { if (typingIndicator) typingIndicator.textContent = ''; }, 3000); } chatInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); chatForm?.dispatchEvent(new Event('submit', { cancelable: true })); } }); chatInput?.addEventListener('input', () => { chatInput.style.height = 'auto'; chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px'; if (chatInput.scrollHeight > 200) { chatInput.style.overflowY = 'auto'; } else { chatInput.style.overflowY = 'hidden'; } if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'typing', channel_id: currentChannel, user_id: window.currentUserId, username: window.currentUsername })); } }); chatForm?.addEventListener('submit', (e) => { e.preventDefault(); const content = chatInput.value.trim(); const file = fileUpload.files[0]; if (!content && !file) return; chatInput.value = ''; chatInput.style.height = '24px'; chatInput.style.overflowY = 'hidden'; const formData = new FormData(); formData.append('content', content); formData.append('channel_id', currentChannel); if (currentThread) { formData.append('thread_id', currentThread); } const progressContainer = document.getElementById('upload-progress-container'); const progressBar = document.getElementById('upload-progress-bar'); const progressPercent = document.getElementById('upload-percentage'); const progressFilename = document.getElementById('upload-filename'); if (file) { formData.append('file', file); fileUpload.value = ''; // Clear file input // Show progress bar progressContainer.style.display = 'block'; progressFilename.textContent = `Uploading: ${file.name}`; progressBar.style.width = '0%'; progressPercent.textContent = '0%'; } const xhr = new XMLHttpRequest(); xhr.open('POST', 'api_v1_messages.php', true); xhr.upload.onprogress = (ev) => { if (ev.lengthComputable && file) { const percent = Math.round((ev.loaded / ev.total) * 100); progressBar.style.width = percent + '%'; progressPercent.textContent = percent + '%'; } }; xhr.onload = () => { if (xhr.status === 200) { const result = JSON.parse(xhr.responseText); if (result.success) { appendMessage(result.message); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'message', data: JSON.stringify({ ...result.message, channel_id: currentChannel }) })); } } else { alert(result.error || 'Failed to send message'); } } progressContainer.style.display = 'none'; }; xhr.onerror = () => { console.error('XHR Error'); progressContainer.style.display = 'none'; alert('An error occurred during the upload.'); }; xhr.send(formData); }); // Handle Click Events document.addEventListener('click', (e) => { console.log('Global click at:', e.target); // Voice Channel Click const voiceItem = e.target.closest('.voice-item'); if (voiceItem) { e.preventDefault(); console.log('Voice item clicked, Channel ID:', voiceItem.dataset.channelId); const channelId = voiceItem.dataset.channelId; if (voiceHandler) { if (voiceHandler.currentChannelId == channelId) { console.log('Already in this channel:', channelId); return; } else { console.log('Joining voice channel:', channelId); voiceHandler.join(channelId); // Update active state in UI document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active')); voiceItem.classList.add('active'); } } else { console.error('voiceHandler not initialized'); } return; } const badge = e.target.closest('.reaction-badge'); if (badge) { const msgId = badge.parentElement.dataset.messageId; const emoji = badge.dataset.emoji; toggleReaction(msgId, emoji); return; } const addBtn = e.target.closest('.add-reaction-btn'); if (addBtn) { const msgId = addBtn.parentElement.dataset.messageId; showEmojiPicker(addBtn, (emoji) => toggleReaction(msgId, emoji)); return; } }); async function toggleReaction(messageId, emoji) { try { const resp = await fetch('api_v1_reactions.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message_id: messageId, emoji: emoji }) }); const result = await resp.json(); if (result.success) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'reaction', message_id: messageId, reactions: result.reactions })); } updateReactionUI(messageId, result.reactions); } } catch (e) { console.error(e); } } function updateReactionUI(messageId, reactions) { const container = document.querySelector(`.message-reactions[data-message-id="${messageId}"]`); if (!container) return; const addBtn = container.querySelector('.add-reaction-btn'); container.innerHTML = ''; reactions.forEach(r => { const badge = document.createElement('span'); const userList = r.users.split(','); const active = userList.includes(String(window.currentUserId)); badge.className = `reaction-badge ${active ? 'active' : ''}`; badge.dataset.emoji = r.emoji; badge.innerHTML = `${r.emoji} ${r.count}`; container.appendChild(badge); }); container.appendChild(addBtn); } function updatePresenceUI(userId, status) { const memberItem = document.querySelector(`.start-dm-btn[data-user-id="${userId}"] .message-avatar`); if (memberItem) { let indicator = memberItem.querySelector('.presence-indicator'); if (!indicator) { indicator = document.createElement('div'); indicator.className = 'presence-indicator'; memberItem.appendChild(indicator); } indicator.style.position = 'absolute'; indicator.style.bottom = '0'; indicator.style.right = '0'; indicator.style.width = '10px'; indicator.style.height = '10px'; indicator.style.borderRadius = '50%'; indicator.style.border = '2px solid var(--bg-members)'; indicator.style.backgroundColor = status === 'online' ? '#23a559' : '#80848e'; } } // Presence indicators initialization (can be expanded) if (window.currentUserId) { // ... (existing presence logic if any) } // Message Actions (Edit/Delete) document.addEventListener('click', async (e) => { const editBtn = e.target.closest('.action-btn.edit'); if (editBtn) { const msgId = editBtn.dataset.id; const msgItem = editBtn.closest('.message-item'); const textEl = msgItem.querySelector('.message-text'); const originalContent = msgItem.dataset.rawContent || textEl.innerText; const input = document.createElement('textarea'); input.className = 'form-control bg-dark text-white'; input.style.resize = 'none'; input.style.overflowY = 'hidden'; input.rows = 1; input.value = originalContent; textEl.innerHTML = ''; textEl.appendChild(input); const resizeInput = () => { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 200) + 'px'; input.style.overflowY = input.scrollHeight > 200 ? 'auto' : 'hidden'; }; input.addEventListener('input', resizeInput); setTimeout(resizeInput, 0); input.focus(); input.setSelectionRange(input.value.length, input.value.length); input.onkeydown = async (ev) => { if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); const newContent = input.value.trim(); if (newContent && newContent !== originalContent) { const resp = await fetch('api_v1_messages.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: msgId, content: newContent }) }); if ((await resp.json()).success) { textEl.innerHTML = parseCustomEmotes(newContent); msgItem.dataset.rawContent = newContent; ws?.send(JSON.stringify({ type: 'message_edit', message_id: msgId, content: newContent })); } } else { textEl.innerHTML = parseCustomEmotes(originalContent); } } else if (ev.key === 'Escape') { textEl.innerHTML = parseCustomEmotes(originalContent); } }; return; } const deleteBtn = e.target.closest('.action-btn.delete'); if (deleteBtn) { if (!confirm('Delete this message?')) return; const msgId = deleteBtn.dataset.id; const resp = await fetch('api_v1_messages.php', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: msgId }) }); if ((await resp.json()).success) { deleteBtn.closest('.message-item').remove(); ws?.send(JSON.stringify({ type: 'message_delete', message_id: msgId })); } return; } const pinBtn = e.target.closest('.action-btn.pin'); if (pinBtn) { const msgId = pinBtn.dataset.id; const isPinned = pinBtn.dataset.pinned == '1'; const action = isPinned ? 'unpin' : 'pin'; const resp = await fetch('api_v1_messages.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: msgId, action: action }) }); const result = await resp.json(); if (result.success) { location.reload(); // Simplest way to reflect changes across UI } return; } const pinnedMessagesBtn = document.getElementById('pinned-messages-btn'); if (e.target.closest('#pinned-messages-btn')) { const container = document.getElementById('pinned-messages-container'); container.innerHTML = '
Loading pinned messages...
'; const modal = new bootstrap.Modal(document.getElementById('pinnedMessagesModal')); modal.show(); const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&pinned=1`); const data = await resp.json(); if (data.success && data.messages.length > 0) { container.innerHTML = ''; data.messages.forEach(msg => { const div = document.createElement('div'); div.className = 'message-item p-2 border-bottom border-secondary'; div.dataset.id = msg.id; div.dataset.rawContent = msg.content; div.style.backgroundColor = 'transparent'; const authorStyle = msg.role_color ? `color: ${msg.role_color};` : ''; div.innerHTML = `
${escapeHTML(msg.username)} ${renderRoleIconJS(msg.role_icon, '14px')} ${msg.time}
${parseCustomEmotes(msg.content)}
`; container.appendChild(div); }); } else { container.innerHTML = '
No pinned messages in this channel.
'; } return; } // Member Menu const memberItem = e.target.closest('.member-item'); if (memberItem) { const userId = memberItem.dataset.userId; const username = memberItem.dataset.username; const avatar = memberItem.dataset.avatar; const roleIds = (memberItem.dataset.roleIds || '').split(',').filter(id => id); // Create or show member menu document.querySelector('.member-context-menu')?.remove(); const menu = document.createElement('div'); menu.className = 'member-context-menu bg-dark border border-secondary rounded p-2'; menu.style.position = 'fixed'; menu.style.zIndex = '1000'; menu.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)'; menu.style.minWidth = '180px'; const rect = memberItem.getBoundingClientRect(); menu.style.top = `${rect.top}px`; menu.style.left = `${rect.left - 190}px`; let rolesHtml = ''; if (roleIds.length > 0) { // Deduplicate and filter valid roles from serverRoles const uniqueRoleIds = [...new Set(roleIds)]; const roles = uniqueRoleIds.map(id => serverRoles.find(r => r.id == id)).filter(r => r); if (roles.length > 0) { rolesHtml = `
Rรดles
${roles.map(r => ` ${r.icon_url ? `` : ''} ${escapeHTML(r.name)} `).join('')}
`; } } menu.innerHTML = `
${escapeHTML(username)}
${rolesHtml} ${(window.isServerOwner || window.canManageServer) ? `` : ''} `; document.body.appendChild(menu); // Close menu on click outside const closeMenu = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('mousedown', closeMenu); } }; document.addEventListener('mousedown', closeMenu); menu.querySelectorAll('.member-menu-action').forEach(btn => { btn.onclick = async () => { const action = btn.dataset.action; if (action === 'message') { const formData = new FormData(); formData.append('user_id', userId); const resp = await fetch('api_v1_dms.php', { method: 'POST', body: formData }); const result = await resp.json(); if (result.success) { window.location.href = `?server_id=dms&channel_id=${result.channel_id}`; } } else if (action === 'edit-roles') { openEditUserRolesModal(userId, username, avatar); } menu.remove(); }; }); } }); // Global Search const searchInput = document.getElementById('global-search'); const searchType = document.getElementById('search-type'); const searchResults = document.getElementById('search-results'); searchInput?.addEventListener('input', async () => { const q = searchInput.value.trim(); const type = searchType.value; if (q.length < 2) { searchResults.style.display = 'none'; return; } const resp = await fetch(`api_v1_search.php?q=${encodeURIComponent(q)}&type=${type}&channel_id=${currentChannel}`); const data = await resp.json(); if (data.success && data.results.length > 0) { searchResults.innerHTML = ''; data.results.forEach(res => { const item = document.createElement('div'); item.className = 'search-result-item d-flex align-items-center gap-2'; if (type === 'users') { item.innerHTML = `
${res.username}
Click to start conversation
`; item.onclick = () => { const formData = new FormData(); formData.append('user_id', res.id); fetch('api_v1_dms.php', { method: 'POST', body: formData }) .then(r => r.json()) .then(resDM => { if (resDM.success) window.location.href = `?server_id=dms&channel_id=${resDM.channel_id}`; }); }; } else { item.innerHTML = `
${escapeHTML(res.username)}
${parseCustomEmotes(res.content)}
`; } searchResults.appendChild(item); }); searchResults.style.display = 'block'; } else { searchResults.innerHTML = '
No results found
'; searchResults.style.display = 'block'; } }); // Channel Permissions Management const channelPermissionsTabBtn = document.getElementById('channel-permissions-tab-btn'); const channelPermissionsRolesList = document.getElementById('channel-permissions-roles-list'); const addPermRoleList = document.getElementById('add-permission-role-list'); const channelPermissionsSettings = document.getElementById('channel-permissions-settings'); const noRoleSelectedView = document.getElementById('no-role-selected-view'); const selectedPermRoleName = document.getElementById('selected-perm-role-name'); const removeSelectedPermRole = document.getElementById('remove-selected-perm-role'); const permissionsTogglesContainer = document.getElementById('permissions-toggles-container'); let currentSelectedOverrideRole = null; let channelPermissionsData = []; channelPermissionsTabBtn?.addEventListener('click', async () => { const channelId = document.getElementById('edit-channel-id').value; currentSelectedOverrideRole = null; channelPermissionsSettings.classList.add('d-none'); noRoleSelectedView.classList.remove('d-none'); await loadChannelPermissions(channelId); await loadRolesForPermissions(channelId); }); const searchChannelPerms = document.getElementById('search-channel-perms'); searchChannelPerms?.addEventListener('input', () => { const query = searchChannelPerms.value.toLowerCase(); const items = channelPermissionsRolesList.querySelectorAll('.list-group-item'); items.forEach(item => { const name = item.textContent.toLowerCase(); if (name.includes(query)) { item.classList.remove('d-none'); } else { item.classList.add('d-none'); } }); }); async function loadChannelPermissions(channelId) { channelPermissionsRolesList.innerHTML = '
Loading...
'; const resp = await fetch(`api_v1_channel_permissions.php?channel_id=${channelId}`); const data = await resp.json(); if (data.success) { channelPermissionsData = data.permissions; renderRoleOverridesList(channelId); } } async function loadRolesForPermissions(channelId) { if (!addPermRoleList) return; addPermRoleList.innerHTML = '
  • Loading...
  • '; try { const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`); const data = await resp.json(); if (data.success) { addPermRoleList.innerHTML = ''; // Roles Section const existingRoleIds = channelPermissionsData.filter(p => p.type === 'role').map(p => parseInt(p.role_id)); const availableRoles = data.roles.filter(role => !existingRoleIds.includes(parseInt(role.id))); if (availableRoles.length > 0) { const header = document.createElement('li'); header.innerHTML = ''; addPermRoleList.appendChild(header); availableRoles.forEach(role => { const li = document.createElement('li'); li.innerHTML = `
    ${role.name}
    `; li.onclick = async (e) => { e.preventDefault(); const postResp = await fetch('api_v1_channel_permissions.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: channelId, role_id: role.id, allow: 0, deny: 0 }) }); const postData = await postResp.json(); if (postData.success) { await loadChannelPermissions(channelId); await loadRolesForPermissions(channelId); selectOverrideItem(role.id, role.name, 'role'); } }; addPermRoleList.appendChild(li); }); } // Members Section const existingUserIds = channelPermissionsData.filter(p => p.type === 'member').map(p => parseInt(p.user_id)); const availableMembers = data.members.filter(m => !existingUserIds.includes(parseInt(m.id))); if (availableMembers.length > 0) { const header = document.createElement('li'); header.innerHTML = ''; addPermRoleList.appendChild(header); availableMembers.forEach(m => { const li = document.createElement('li'); li.innerHTML = ` ${m.username} `; li.onclick = async (e) => { e.preventDefault(); const postResp = await fetch('api_v1_channel_permissions.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: channelId, user_id: m.id, allow: 0, deny: 0 }) }); const postData = await postResp.json(); if (postData.success) { await loadChannelPermissions(channelId); await loadRolesForPermissions(channelId); selectOverrideItem(m.id, m.username, 'member'); } }; addPermRoleList.appendChild(li); }); } } } catch (err) { console.error(err); } } function renderRoleOverridesList(channelId) { channelPermissionsRolesList.innerHTML = ''; if (channelPermissionsData.length === 0) { channelPermissionsRolesList.innerHTML = '
    No overrides configured.
    '; return; } const sortedData = [...channelPermissionsData].sort((a, b) => { if (a.type !== b.type) return a.type === 'role' ? -1 : 1; const nameA = (a.display_name || '').toLowerCase(); const nameB = (b.display_name || '').toLowerCase(); const isAEveryone = nameA.includes('everyone'); const isBEveryone = nameB.includes('everyone'); if (isAEveryone && !isBEveryone) return -1; if (!isAEveryone && isBEveryone) return 1; return nameA.localeCompare(nameB); }); sortedData.forEach(p => { const item = document.createElement('div'); const isActive = currentSelectedOverrideRole == (p.type === 'role' ? p.role_id : p.user_id) && currentSelectedOverrideType === p.type; item.className = `list-group-item list-group-item-action bg-transparent text-white border-0 mb-1 p-2 small d-flex align-items-center ${isActive ? 'active' : ''}`; item.style.cursor = 'pointer'; let icon = ''; if (p.type === 'role') { icon = `
    `; } else { icon = ``; } item.innerHTML = ` ${icon} ${p.display_name} `; item.onclick = () => selectOverrideItem(p.type === 'role' ? p.role_id : p.user_id, p.display_name, p.type); channelPermissionsRolesList.appendChild(item); }); } let currentSelectedOverrideType = 'role'; function selectOverrideItem(id, name, type) { currentSelectedOverrideRole = id; currentSelectedOverrideType = type; const channelId = document.getElementById('edit-channel-id').value; renderRoleOverridesList(channelId); selectedPermRoleName.textContent = name; noRoleSelectedView.classList.add('d-none'); channelPermissionsSettings.classList.remove('d-none'); const p = channelPermissionsData.find(perm => { if (type === 'role') return perm.role_id == id && perm.type === 'role'; return perm.user_id == id && perm.type === 'member'; }) || { allow_permissions: 0, deny_permissions: 0 }; document.querySelectorAll('.perm-tri-state').forEach(group => { const bit = parseInt(group.dataset.permBit); updateToggleUI(bit, p.allow_permissions, p.deny_permissions); }); } function updateToggleUI(bit, allowPerms, denyPerms) { const group = document.querySelector(`.perm-tri-state[data-perm-bit="${bit}"]`); if (!group) return; if (allowPerms & bit) { group.querySelector('input[value="allow"]').checked = true; } else if (denyPerms & bit) { group.querySelector('input[value="deny"]').checked = true; } else { group.querySelector('input[value="neutral"]').checked = true; } } removeSelectedPermRole?.addEventListener('click', async () => { if (!currentSelectedOverrideRole) return; const channelId = document.getElementById('edit-channel-id').value; const payload = { channel_id: channelId }; if (currentSelectedOverrideType === 'role') { payload.role_id = currentSelectedOverrideRole; } else { payload.user_id = currentSelectedOverrideRole; } await fetch('api_v1_channel_permissions.php', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); currentSelectedOverrideRole = null; channelPermissionsSettings.classList.add('d-none'); noRoleSelectedView.classList.remove('d-none'); loadChannelPermissions(channelId); }); permissionsTogglesContainer?.addEventListener('change', async (e) => { if (e.target.type === 'radio') { const group = e.target.closest('.perm-tri-state'); const bit = parseInt(group.dataset.permBit); const val = e.target.value; const channelId = document.getElementById('edit-channel-id').value; const id = currentSelectedOverrideRole; const type = currentSelectedOverrideType; let p = channelPermissionsData.find(perm => { if (type === 'role') return perm.role_id == id && perm.type === 'role'; return perm.user_id == id && perm.type === 'member'; }); if (!p) { p = { channel_id: channelId, allow_permissions: 0, deny_permissions: 0, type: type }; if (type === 'role') p.role_id = id; else p.user_id = id; channelPermissionsData.push(p); } let allow = parseInt(p.allow_permissions); let deny = parseInt(p.deny_permissions); // Clear current bit allow &= ~bit; deny &= ~bit; if (val === 'allow') allow |= bit; if (val === 'deny') deny |= bit; const payload = { channel_id: channelId, allow, deny }; if (type === 'role') payload.role_id = id; else payload.user_id = id; await fetch('api_v1_channel_permissions.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); // Update local data p.allow_permissions = allow; p.deny_permissions = deny; } }); document.addEventListener('click', async (e) => { if (!e.target.closest('.search-container')) { searchResults.style.display = 'none'; } if (e.target.classList.contains('move-rule-btn')) { if (!window.canManageChannels) return; const id = e.target.dataset.id; const dir = e.target.dataset.dir; const resp = await fetch('api_v1_rules.php', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, dir }) }); if ((await resp.json()).success) location.reload(); } }); // Roles Management const channelSettingsBtns = document.querySelectorAll('.channel-settings-btn'); channelSettingsBtns.forEach(btn => { btn.addEventListener('click', () => { const modal = document.getElementById('editChannelModal'); const channelId = btn.dataset.id; const channelType = btn.dataset.type || 'chat'; const channelName = btn.dataset.name; modal.querySelector('#edit-channel-id').value = channelId; modal.querySelector('#edit-channel-name').value = channelName; modal.querySelector('#header-channel-name').textContent = channelName; modal.querySelector('#edit-channel-type').value = channelType; // Force switch to Overview tab const overviewTabBtn = modal.querySelector('[data-bs-target="#edit-channel-general"]'); if (overviewTabBtn) { bootstrap.Tab.getOrCreateInstance(overviewTabBtn).show(); } modal.querySelector('#edit-channel-files').checked = btn.dataset.files == '1'; modal.querySelector('#edit-channel-limit').value = btn.dataset.limit || ''; modal.querySelector('#edit-channel-status').value = btn.dataset.status || ''; modal.querySelector('#edit-channel-icon').value = btn.dataset.icon || ''; modal.querySelector('#edit-channel-rules-role').value = btn.dataset.rulesRole || ''; modal.querySelector('#edit-channel-category-id').value = btn.dataset.category || ''; modal.querySelector('#delete-channel-id').value = channelId; // Check if channel is named "rรดle" or "role" const isRoleChannel = channelName.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "") === "role"; // Toggle rules role visibility const rulesRoleContainer = document.getElementById('edit-channel-rules-role-container'); if (rulesRoleContainer) { rulesRoleContainer.style.display = (channelType === 'rules') ? 'block' : 'none'; } // Hide limit, files and clear chat for rules, autorole, and role channels const editLimitContainer = document.getElementById('edit-channel-limit-container'); const editFilesContainer = document.getElementById('edit-channel-files-container'); const clearChatBtn = document.getElementById('clear-channel-history-btn'); const hideExtra = (channelType === 'rules' || channelType === 'autorole' || isRoleChannel); if (editLimitContainer) editLimitContainer.style.display = hideExtra ? 'none' : 'block'; if (editFilesContainer) editFilesContainer.style.display = hideExtra ? 'none' : 'block'; if (clearChatBtn) clearChatBtn.style.display = (channelType === 'rules') ? 'none' : 'inline-block'; // Reset delete zone document.getElementById('delete-confirm-zone').style.display = 'none'; // Show/Hide RSS tab const rssTabNav = document.getElementById('rss-tab-nav'); const statusContainer = document.getElementById('edit-channel-status-container'); if (channelType === 'announcement') { rssTabNav.style.display = 'block'; } else { rssTabNav.style.display = 'none'; // Switch to General tab if we were on RSS const rssTabBtn = document.getElementById('rss-tab-btn'); if (rssTabBtn && rssTabBtn.classList.contains('active')) { bootstrap.Tab.getOrCreateInstance(modal.querySelector('[data-bs-target="#edit-channel-general"]')).show(); } } if (channelType === 'voice') { statusContainer.style.display = 'block'; } else { statusContainer.style.display = 'none'; } }); }); document.getElementById('delete-channel-trigger')?.addEventListener('click', () => { document.getElementById('delete-confirm-zone').style.display = 'block'; }); // RSS Management const editChannelType = document.getElementById('edit-channel-type'); editChannelType?.addEventListener('change', () => { const type = editChannelType.value; const rssTabNav = document.getElementById('rss-tab-nav'); const statusContainer = document.getElementById('edit-channel-status-container'); const channelName = document.getElementById('edit-channel-name').value; const isRoleChannel = channelName.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "") === "role"; rssTabNav.style.display = (type === 'announcement') ? 'block' : 'none'; statusContainer.style.display = (type === 'voice') ? 'block' : 'none'; // Rules specific visibility const rulesRoleContainer = document.getElementById('edit-channel-rules-role-container'); if (rulesRoleContainer) { rulesRoleContainer.style.display = (type === 'rules') ? 'block' : 'none'; } const editLimitContainer = document.getElementById('edit-channel-limit-container'); const editFilesContainer = document.getElementById('edit-channel-files-container'); const clearChatBtn = document.getElementById('clear-channel-history-btn'); const hideExtra = (type === 'rules' || type === 'autorole' || isRoleChannel); if (editLimitContainer) editLimitContainer.style.display = hideExtra ? 'none' : 'block'; if (editFilesContainer) editFilesContainer.style.display = hideExtra ? 'none' : 'block'; if (clearChatBtn) clearChatBtn.style.display = (type === 'rules') ? 'none' : 'inline-block'; }); // RSS Management const rssTabBtn = document.getElementById('rss-tab-btn'); const rssFeedsList = document.getElementById('rss-feeds-list'); const addRssBtn = document.getElementById('add-rss-btn'); const syncRssBtn = document.getElementById('sync-rss-btn'); rssTabBtn?.addEventListener('click', loadRssFeeds); async function loadRssFeeds() { const channelId = document.getElementById('edit-channel-id').value; rssFeedsList.innerHTML = '
    Loading feeds...
    '; try { const resp = await fetch(`api_v1_rss.php?channel_id=${channelId}`); const data = await resp.json(); if (data.success) { renderRssFeeds(data.feeds); } } catch (e) { console.error(e); } } function renderRssFeeds(feeds) { rssFeedsList.innerHTML = ''; if (feeds.length === 0) { rssFeedsList.innerHTML = '
    No RSS feeds configured.
    '; return; } feeds.forEach(feed => { const item = document.createElement('div'); item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-1'; item.innerHTML = `
    ${feed.url}
    Last fetched: ${feed.last_fetched_at || 'Never'}
    `; rssFeedsList.appendChild(item); }); } addRssBtn?.addEventListener('click', async () => { const channelId = document.getElementById('edit-channel-id').value; const url = document.getElementById('new-rss-url').value.trim(); if (!url) return; const formData = new FormData(); formData.append('action', 'add'); formData.append('channel_id', channelId); formData.append('url', url); const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData }); if ((await resp.json()).success) { document.getElementById('new-rss-url').value = ''; loadRssFeeds(); } }); syncRssBtn?.addEventListener('click', async () => { const channelId = document.getElementById('edit-channel-id').value; syncRssBtn.disabled = true; syncRssBtn.textContent = 'Syncing...'; const formData = new FormData(); formData.append('action', 'sync'); formData.append('channel_id', channelId); try { const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData }); const result = await resp.json(); if (result.success) { alert(`Sync complete! Found ${result.new_items} new items.`); loadRssFeeds(); } } catch (e) { console.error(e); } syncRssBtn.disabled = false; syncRssBtn.textContent = 'Sync Now'; }); rssFeedsList?.addEventListener('click', async (e) => { if (e.target.classList.contains('delete-rss-btn')) { const channelId = document.getElementById('edit-channel-id').value; const feedId = e.target.dataset.id; const formData = new FormData(); formData.append('action', 'delete'); formData.append('channel_id', channelId); formData.append('feed_id', feedId); await fetch('api_v1_rss.php', { method: 'POST', body: formData }); loadRssFeeds(); } }); // Auto-sync for announcement channels if (window.activeChannelId) { const autoSyncRss = async () => { // Check if we are in an announcement channel // We can look for the bullhorn icon in the header const headerIcon = document.querySelector('.chat-header i.fa-bullhorn'); if (headerIcon) { const formData = new FormData(); formData.append('action', 'sync'); formData.append('channel_id', window.activeChannelId); formData.append('auto', '1'); try { const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData }); const data = await resp.json(); if (data.success && data.new_items > 0) { // If new items were found, we might want to refresh the message list // but only if we are still on the same channel if (window.activeChannelId == data.channel_id) { // For now, we don't reload automatically to avoid interrupting the user // The new messages will appear on next reload or polling if implemented } } } catch (e) { } } }; // Initial sync setTimeout(autoSyncRss, 2000); // Periodic sync every 2 minutes setInterval(autoSyncRss, 120000); } // Clear Channel History const clearHistoryBtn = document.getElementById('clear-channel-history-btn'); clearHistoryBtn?.addEventListener('click', async () => { const channelId = document.getElementById('edit-channel-id').value; if (!confirm('Voulez-vous vraiment vider tout l\'historique de ce salon ? Cette action est irrรฉversible.')) return; try { const formData = new FormData(); formData.append('channel_id', channelId); const resp = await fetch('api_v1_clear_channel.php', { method: 'POST', body: formData }); const result = await resp.json(); if (result.success) { location.reload(); } else { alert(result.error || 'Erreur lors du nettoyage de l\'historique'); } } catch (e) { console.error(e); } }); // Roles Management const rolesTabBtn = document.getElementById('roles-tab-btn'); const rolesList = document.getElementById('roles-list'); const addRoleBtn = document.getElementById('add-role-btn'); const membersTabBtn = document.getElementById('members-tab-btn'); const membersList = document.getElementById('server-members-list'); const activeServerId = window.activeServerId || new URLSearchParams(window.location.search).get('server_id') || 1; let serverRoles = []; let serverPermissions = []; rolesTabBtn?.addEventListener('click', loadRoles); membersTabBtn?.addEventListener('click', loadRoles); // Both tabs need roles data async function loadRoles() { try { const channelIdParam = window.activeChannelId ? `&channel_id=${window.activeChannelId}` : ''; const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}${channelIdParam}`); const data = await resp.json(); if (data.success) { serverRoles = data.roles; serverPermissions = data.permissions_list; if (rolesList) renderRoles(data.roles); if (membersList) renderMembers(data.members); // Use filtered members for sidebar, all members for colors updateGlobalUI(data.filtered_members || data.members, data.members); } } catch (e) { console.error(e); } } function renderRoleIconJS(icon, size = '14px') { if (!icon) return ''; const isUrl = icon.startsWith('http') || icon.startsWith('/'); if (isUrl) { return ``; } else if (icon.startsWith(':') && icon.endsWith(':')) { const ce = (window.CUSTOM_EMOTES_CACHE || []).find(e => e.code === icon); if (ce) { return ``; } return `${escapeHTML(icon)}`; } else { return `${escapeHTML(icon)}`; } } function updateGlobalUI(sidebarMembers, allMembers = null) { if (!allMembers) allMembers = sidebarMembers; // Update members sidebar const sidebar = document.querySelector('.members-sidebar'); if (sidebar) { const countEl = sidebar.querySelector('div[style*="text-transform: uppercase"]'); if (countEl) countEl.textContent = `Members โ€” ${sidebarMembers.length}`; // We need to keep the "Members - X" div and replace everything else const header = sidebar.firstElementChild; sidebar.innerHTML = ''; sidebar.appendChild(header); sidebarMembers.forEach(m => { const item = document.createElement('div'); item.className = 'channel-item member-item'; item.dataset.userId = m.id; item.dataset.username = m.username; item.dataset.avatar = m.avatar_url || ''; item.dataset.roleIds = m.role_ids || ''; item.style.color = 'var(--text-primary)'; item.style.marginBottom = '8px'; item.style.cursor = 'pointer'; const roleIconHtml = renderRoleIconJS(m.role_icon, '14px'); const avatarBg = m.avatar_url ? `background-image: url('${m.avatar_url}');` : ''; const statusColor = m.status === 'online' ? '#23a559' : '#80848e'; item.innerHTML = `
    ${m.status === 'online' ? `
    ` : ''}
    ${escapeHTML(m.username)} ${roleIconHtml} `; sidebar.appendChild(item); }); } // Update chat colors document.querySelectorAll('.message-author').forEach(authorEl => { const username = authorEl.childNodes[0].textContent.trim(); const member = allMembers.find(m => m.username === username); if (member) { authorEl.style.color = member.role_color || 'inherit'; // Try to update icon if it exists or add it if it doesn't let iconEl = authorEl.querySelector('.role-icon, span.ms-1'); const newIconHtml = renderRoleIconJS(member.role_icon, '14px'); if (newIconHtml) { if (iconEl) { const temp = document.createElement('div'); temp.innerHTML = newIconHtml; iconEl.replaceWith(temp.firstChild); } else { const temp = document.createElement('div'); temp.innerHTML = newIconHtml; // Insert after the text node authorEl.insertBefore(temp.firstChild, authorEl.childNodes[1]); } } else if (iconEl) { iconEl.remove(); } } }); } function renderRoles(roles) { rolesList.innerHTML = ''; if (roles.length === 0) { rolesList.innerHTML = '
    No roles created yet.
    '; } roles.forEach(role => { const item = document.createElement('div'); item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-1 rounded role-sortable-item'; item.dataset.id = role.id; const roleIconHtml = renderRoleIconJS(role.icon_url, '14px'); item.innerHTML = `
    ${role.name} ${roleIconHtml}
    `; rolesList.appendChild(item); }); // Initialize Sortable for roles if (typeof Sortable !== 'undefined' && rolesList) { new Sortable(rolesList, { animation: 150, handle: '.role-drag-handle', ghostClass: 'sortable-ghost', onEnd: () => saveRolePositions() }); } } async function saveRolePositions() { const orders = []; const items = rolesList.querySelectorAll('.role-sortable-item'); // Invert the order because we ORDER BY position DESC in SQL let position = items.length - 1; items.forEach(item => { orders.push({ id: item.dataset.id, position: position-- }); }); try { await fetch('api_v1_roles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'reorder', server_id: activeServerId, orders: orders }) }); } catch (e) { console.error(e); } } function renderMembers(members) { membersList.innerHTML = ''; members.forEach(member => { const item = document.createElement('div'); item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-2 rounded bg-dark'; const roleIconHtml = renderRoleIconJS(member.role_icon, '14px'); item.innerHTML = `
    ${escapeHTML(member.username)} ${roleIconHtml}
    ${member.role_names ? member.role_names.split(',').join(', ') : 'No roles'}
    ${(window.isServerOwner || window.canManageServer) ? ` ` : ''} `; membersList.appendChild(item); }); } // Add listener for the button in members list tab membersList?.addEventListener('click', (e) => { const btn = e.target.closest('.edit-user-roles-settings-btn'); if (btn) { openEditUserRolesModal(btn.dataset.id, btn.dataset.username, btn.dataset.avatar); } }); // Role Editing Modal Logic rolesList?.addEventListener('click', (e) => { if (e.target.classList.contains('edit-role-btn-v2')) { const role = e.target.dataset; document.getElementById('edit-role-id').value = role.id; document.getElementById('edit-role-name').value = role.name; document.getElementById('edit-role-color').value = role.color; document.getElementById('edit-role-icon').value = role.icon; document.getElementById('selected-role-emoji-preview').textContent = role.icon || ''; const modalTitle = document.querySelector('#roleEditorModal .modal-title'); if (modalTitle) modalTitle.textContent = 'Modifier le rรดle'; const permsContainer = document.getElementById('role-permissions-checkboxes'); permsContainer.innerHTML = ''; const currentPerms = parseInt(role.perms); serverPermissions.forEach(p => { const isChecked = (currentPerms & p.value) === p.value; permsContainer.innerHTML += `
    `; }); const modal = new bootstrap.Modal(document.getElementById('roleEditorModal')); modal.show(); } }); document.getElementById('save-role-btn')?.addEventListener('click', async (e) => { const btn = e.target; const originalText = btn.textContent; const id = document.getElementById('edit-role-id').value; const name = document.getElementById('edit-role-name').value; const color = document.getElementById('edit-role-color').value; const icon_url = document.getElementById('edit-role-icon').value; let permissions = 0; document.querySelectorAll('.perm-check:checked').forEach(cb => { permissions |= parseInt(cb.value); }); try { const action = id ? 'update' : 'create'; const resp = await fetch('api_v1_roles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, server_id: activeServerId, id, name, color, icon_url, permissions }) }); const data = await resp.json(); if (data.success) { btn.textContent = 'Saved โœ…'; btn.classList.replace('btn-primary', 'btn-success'); setTimeout(() => { btn.textContent = originalText; btn.classList.replace('btn-success', 'btn-primary'); }, 2000); loadRoles(); } } catch (e) { console.error(e); } }); async function openEditUserRolesModal(userId, username, avatar) { const modal = document.getElementById('editUserRolesModal'); document.getElementById('edit-user-roles-user-id').value = userId; document.getElementById('edit-user-roles-username').textContent = username; const avatarEl = document.getElementById('edit-user-roles-avatar'); avatarEl.style.backgroundImage = avatar ? `url('${avatar}')` : 'none'; const list = document.getElementById('user-roles-selection-list'); list.innerHTML = '
    Loading roles...
    '; const bsModal = new bootstrap.Modal(modal); bsModal.show(); try { // We need to fetch roles and the current user's roles // We can reuse loadRoles or make a specific call const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`); const data = await resp.json(); if (data.success) { const member = data.members.find(m => m.id == userId); const assignedRoles = member && member.role_ids ? member.role_ids.split(',') : []; list.innerHTML = ''; // Sort roles by position descending for display data.roles.sort((a, b) => b.position - a.position).forEach(role => { const isChecked = assignedRoles.includes(role.id.toString()); const item = document.createElement('div'); item.className = 'list-group-item bg-dark text-white border-secondary p-2 d-flex align-items-center'; item.innerHTML = ` `; list.appendChild(item); }); if (data.roles.length === 0) { list.innerHTML = '
    No roles defined for this server.
    '; } } } catch (e) { console.error(e); } } document.getElementById('save-user-roles-btn')?.addEventListener('click', async (e) => { const btn = e.target; const originalText = btn.textContent; const userId = document.getElementById('edit-user-roles-user-id').value; const roleIds = Array.from(document.querySelectorAll('.user-role-checkbox:checked')).map(cb => cb.value); try { const resp = await fetch('api_v1_roles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'set_user_roles', server_id: activeServerId, user_id: userId, role_ids: roleIds }) }); const data = await resp.json(); if (data.success) { btn.textContent = 'Saved โœ…'; btn.classList.replace('btn-primary', 'btn-success'); setTimeout(() => { btn.textContent = originalText; btn.classList.replace('btn-success', 'btn-primary'); }, 2000); loadRoles(); } else { alert(data.error || 'Failed to update roles'); } } catch (e) { console.error(e); } }); addRoleBtn?.addEventListener('click', async () => { document.getElementById('edit-role-id').value = ''; document.getElementById('edit-role-name').value = 'New Role'; document.getElementById('edit-role-color').value = '#99aab5'; document.getElementById('edit-role-icon').value = ''; document.getElementById('selected-role-emoji-preview').textContent = ''; const modalTitle = document.querySelector('#roleEditorModal .modal-title'); if (modalTitle) modalTitle.textContent = 'Crรฉer un rรดle'; const permsContainer = document.getElementById('role-permissions-checkboxes'); permsContainer.innerHTML = ''; serverPermissions.forEach(p => { permsContainer.innerHTML += `
    `; }); const modal = new bootstrap.Modal(document.getElementById('roleEditorModal')); modal.show(); }); rolesList?.addEventListener('click', async (e) => { if (e.target.classList.contains('delete-role-btn')) { if (!confirm('Delete this role?')) return; const roleId = e.target.dataset.id; const resp = await fetch('api_v1_roles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', server_id: activeServerId, id: roleId }) }); if ((await resp.json()).success) loadRoles(); } }); // Webhooks Management const webhooksTabBtn = document.getElementById('webhooks-tab-btn'); const webhooksList = document.getElementById('webhooks-list'); const addWebhookBtn = document.getElementById('add-webhook-btn'); webhooksTabBtn?.addEventListener('click', loadWebhooks); async function loadWebhooks() { webhooksList.innerHTML = '
    Loading webhooks...
    '; try { const resp = await fetch(`api_v1_webhook.php?server_id=${activeServerId}`); const data = await resp.json(); if (data.success) { renderWebhooks(data.webhooks); } } catch (e) { console.error(e); } } function renderWebhooks(webhooks) { webhooksList.innerHTML = ''; if (webhooks.length === 0) { webhooksList.innerHTML = '
    No webhooks found.
    '; return; } webhooks.forEach(wh => { const item = document.createElement('div'); item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-2'; const url = `${window.location.origin}/api_v1_webhook.php?token=${wh.token}`; item.innerHTML = `
    ${wh.name}
    Channel: #${wh.channel_name}
    `; webhooksList.appendChild(item); }); } addWebhookBtn?.addEventListener('click', async () => { const name = prompt('Webhook name:', 'Bot Name'); if (!name) return; // Fetch channels for this server to let user pick one const respChannels = await fetch(`api_v1_channels.php?server_id=${activeServerId}`); const dataChannels = await respChannels.json(); if (!dataChannels.length) return alert('Create a channel first.'); const channelId = prompt('Enter Channel ID:\n' + dataChannels.map(c => `${c.id}: #${c.name}`).join('\n')); if (!channelId) return; try { const resp = await fetch('api_v1_webhook.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: channelId, name: name }) }); if ((await resp.json()).success) loadWebhooks(); } catch (e) { console.error(e); } }); webhooksList?.addEventListener('click', async (e) => { if (e.target.classList.contains('delete-webhook-btn')) { if (!confirm('Delete this webhook?')) return; const whId = e.target.dataset.id; const resp = await fetch('api_v1_webhook.php', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: whId }) }); if ((await resp.json()).success) loadWebhooks(); } }); // Stats Management const statsTabBtn = document.getElementById('stats-tab-btn'); statsTabBtn?.addEventListener('click', loadStats); async function loadStats() { try { const resp = await fetch(`api_v1_stats.php?server_id=${activeServerId}`); const data = await resp.json(); if (data.success) { document.getElementById('stat-members').textContent = data.stats.total_members; document.getElementById('stat-messages').textContent = data.stats.total_messages; const topUsersList = document.getElementById('top-users-list'); topUsersList.innerHTML = ''; data.stats.top_users.forEach(user => { const item = document.createElement('div'); item.className = 'd-flex justify-content-between align-items-center mb-1 p-2 bg-dark rounded'; item.innerHTML = `${user.username}${user.message_count} msgs`; topUsersList.appendChild(item); }); const activity = document.getElementById('activity-chart-placeholder'); activity.innerHTML = ''; data.stats.history.forEach(day => { const bar = document.createElement('div'); bar.className = 'd-flex align-items-center mb-1'; const percent = Math.min(100, (day.count / 100) * 100); // Normalize to 100 for visual bar.innerHTML = `
    ${day.date}
    ${day.count}
    `; activity.appendChild(bar); }); if (data.stats.history.length === 0) { activity.innerHTML = '
    No activity in the last 7 days.
    '; } } } catch (e) { console.error(e); } } // Server Settings const searchServerIconBtn = document.getElementById('search-server-icon-btn'); const serverIconResults = document.getElementById('server-icon-search-results'); const serverIconPreview = document.getElementById('server-icon-preview'); const serverIconUrlInput = document.getElementById('server-icon-url'); searchServerIconBtn?.addEventListener('click', async () => { const query = prompt('Search for a server icon:', 'abstract'); if (!query) return; serverIconResults.innerHTML = '
    Searching...
    '; try { const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(query)}`); const data = await resp.json(); serverIconResults.innerHTML = ''; data.forEach(photo => { const img = document.createElement('img'); img.src = photo.url; img.className = 'avatar-pick'; img.style.width = '50px'; img.style.height = '50px'; img.onclick = () => { serverIconUrlInput.value = photo.url; serverIconPreview.style.backgroundImage = `url('${photo.url}')`; serverIconResults.innerHTML = ''; }; serverIconResults.appendChild(img); }); } catch (e) { serverIconResults.innerHTML = '
    Error fetching icons
    '; } }); // Forum: New Thread const newThreadBtn = document.getElementById('new-thread-btn'); const newThreadModal = document.getElementById('newThreadModal') ? new bootstrap.Modal(document.getElementById('newThreadModal')) : null; let selectedTagIds = []; newThreadBtn?.addEventListener('click', async () => { if (!newThreadModal) return; // Load tags for this channel const tagsList = document.getElementById('new-thread-tags-list'); tagsList.innerHTML = '
    Loading tags...
    '; selectedTagIds = []; try { const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`); const data = await resp.json(); tagsList.innerHTML = ''; if (data.success && data.tags.length > 0) { data.tags.forEach(tag => { const span = document.createElement('span'); span.className = 'badge rounded-pill p-2 border border-secondary'; span.style.cursor = 'pointer'; span.style.backgroundColor = 'transparent'; span.dataset.id = tag.id; span.dataset.color = tag.color; span.textContent = tag.name; span.onclick = () => { if (selectedTagIds.includes(tag.id)) { selectedTagIds = selectedTagIds.filter(id => id !== tag.id); span.style.backgroundColor = 'transparent'; } else { selectedTagIds.push(tag.id); span.style.backgroundColor = tag.color; } }; tagsList.appendChild(span); }); } else { tagsList.innerHTML = '
    No tags available.
    '; } } catch (e) { console.error(e); } newThreadModal.show(); }); document.getElementById('submit-new-thread-btn')?.addEventListener('click', async () => { const title = document.getElementById('new-thread-title').value.trim(); if (!title) return; try { const formData = new FormData(); formData.append('channel_id', currentChannel); formData.append('title', title); formData.append('tag_ids', selectedTagIds.join(',')); const resp = await fetch('api_v1_threads.php', { method: 'POST', body: formData }); const result = await resp.json(); if (result.success) { window.location.href = `?server_id=${activeServerId}&channel_id=${currentChannel}&thread_id=${result.thread_id}`; } else { alert(result.error || 'Failed to create thread'); } } catch (e) { console.error(e); } }); // Forum: Mark as Solution document.addEventListener('click', async (e) => { const solBtn = e.target.closest('.action-btn.mark-solution'); if (solBtn) { const threadId = solBtn.dataset.threadId; const messageId = solBtn.classList.contains('active') ? null : solBtn.dataset.messageId; try { const resp = await fetch('api_v1_threads.php?action=solve', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thread_id: threadId, message_id: messageId }) }); const result = await resp.json(); if (result.success) { location.reload(); } else { alert(result.error || 'Failed to update solution'); } } catch (e) { console.error(e); } } }); // Forum: Manage Tags const manageTagsBtn = document.getElementById('manage-tags-btn'); const manageTagsModal = document.getElementById('manageTagsModal') ? new bootstrap.Modal(document.getElementById('manageTagsModal')) : null; manageTagsBtn?.addEventListener('click', async () => { if (!manageTagsModal) return; loadForumAdminTags(); manageTagsModal.show(); }); // Forum Thread Actions (Pin/Lock) const pinThreadBtn = document.getElementById('toggle-pin-thread'); pinThreadBtn?.addEventListener('click', async () => { const id = pinThreadBtn.dataset.id; const pinned = pinThreadBtn.dataset.pinned == '1'; const action = pinned ? 'unpin' : 'pin'; try { const resp = await fetch(`api_v1_threads.php?action=${action}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thread_id: id }) }); const result = await resp.json(); if (result.success) location.reload(); else alert(result.error || 'Failed to update thread'); } catch (e) { console.error(e); } }); const lockThreadBtn = document.getElementById('toggle-lock-thread'); lockThreadBtn?.addEventListener('click', async () => { const id = lockThreadBtn.dataset.id; const locked = lockThreadBtn.dataset.locked == '1'; const action = locked ? 'unlock' : 'lock'; try { const resp = await fetch(`api_v1_threads.php?action=${action}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thread_id: id }) }); const result = await resp.json(); if (result.success) location.reload(); else alert(result.error || 'Failed to update thread'); } catch (e) { console.error(e); } }); const deleteThreadBtn = document.getElementById('delete-thread-btn'); deleteThreadBtn?.addEventListener('click', async () => { if (!confirm('Are you sure you want to delete this thread? This action cannot be undone.')) return; const id = deleteThreadBtn.dataset.id; const channelId = deleteThreadBtn.dataset.channelId; const serverId = deleteThreadBtn.dataset.serverId; try { const resp = await fetch(`api_v1_threads.php?action=delete`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thread_id: id }) }); const result = await resp.json(); if (result.success) { location.href = `?server_id=${serverId}&channel_id=${channelId}`; } else { alert(result.error || 'Failed to delete thread'); } } catch (e) { console.error(e); } }); async function loadForumAdminTags() { const list = document.getElementById('forum-tags-admin-list'); list.innerHTML = '
    Loading tags...
    '; try { const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`); const data = await resp.json(); list.innerHTML = ''; if (data.success && data.tags.length > 0) { data.tags.forEach(tag => { const div = document.createElement('div'); div.className = 'd-flex justify-content-between align-items-center mb-2 p-2 bg-dark rounded'; div.innerHTML = `
    ${tag.name}
    `; list.appendChild(div); }); } else { list.innerHTML = '
    No tags created yet.
    '; } } catch (e) { console.error(e); } } document.getElementById('add-forum-tag-btn')?.addEventListener('click', async () => { const name = document.getElementById('new-tag-name').value.trim(); const color = document.getElementById('new-tag-color').value; if (!name) return; try { const resp = await fetch('api_v1_tags.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'create', channel_id: currentChannel, name, color }) }); if ((await resp.json()).success) { document.getElementById('new-tag-name').value = ''; loadForumAdminTags(); } } catch (e) { console.error(e); } }); document.getElementById('forum-tags-admin-list')?.addEventListener('click', async (e) => { if (e.target.classList.contains('delete-forum-tag-btn')) { const tagId = e.target.dataset.id; try { const resp = await fetch('api_v1_tags.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', channel_id: currentChannel, tag_id: tagId }) }); if ((await resp.json()).success) loadForumAdminTags(); } catch (e) { console.error(e); } } }); // Rules: Add Rule const addRuleBtn = document.getElementById('add-rule-btn'); const addRuleForm = document.getElementById('add-rule-form'); const newRuleContent = document.getElementById('new-rule-content'); const saveNewRuleBtn = document.getElementById('save-new-rule-btn'); const cancelNewRuleBtn = document.getElementById('cancel-new-rule-btn'); addRuleBtn?.addEventListener('click', () => { addRuleBtn.style.display = 'none'; addRuleForm.style.display = 'block'; newRuleContent.focus(); }); cancelNewRuleBtn?.addEventListener('click', () => { addRuleBtn.style.display = 'block'; addRuleForm.style.display = 'none'; newRuleContent.value = ''; }); saveNewRuleBtn?.addEventListener('click', async () => { const content = newRuleContent.value.trim(); if (!content) return; try { const formData = new FormData(); formData.append('channel_id', currentChannel); formData.append('content', content); const resp = await fetch('api_v1_rules.php', { method: 'POST', body: formData }); const result = await resp.json(); if (result.success) { location.reload(); } else { alert(result.error || 'Failed to add rule'); } } catch (e) { console.error(e); } }); const rulesListSortable = document.getElementById('rules-list-sortable'); if (typeof Sortable !== 'undefined' && rulesListSortable && window.canManageChannels) { new Sortable(rulesListSortable, { animation: 150, ghostClass: 'sortable-ghost', onEnd: async () => { const order = Array.from(rulesListSortable.querySelectorAll('.rule-item')).map(el => el.dataset.id); // Update numbers in UI rulesListSortable.querySelectorAll('.rule-item').forEach((item, index) => { const numEl = item.querySelector('.rule-number'); if (numEl) numEl.textContent = `${index + 1}.`; }); await fetch('api_v1_rules.php', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order: order }) }); } }); } // Rules: Delete/Edit document.addEventListener('click', async (e) => { if (e.target.classList.contains('delete-rule-btn')) { if (!confirm('Delete this rule?')) return; const id = e.target.dataset.id; const resp = await fetch(`api_v1_rules.php?id=${id}`, { method: 'DELETE' }); if ((await resp.json()).success) location.reload(); } if (e.target.classList.contains('edit-rule-btn')) { const id = e.target.dataset.id; const oldContent = e.target.closest('.rule-item').querySelector('.rule-content').innerText; const newContent = prompt('Edit Rule:', oldContent); if (!newContent || newContent === oldContent) return; const resp = await fetch('api_v1_rules.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, content: newContent }) }); if ((await resp.json()).success) location.reload(); } }); // Rules Acceptance document.getElementById('accept-rules-btn')?.addEventListener('click', async () => { const btn = document.getElementById('accept-rules-btn'); btn.disabled = true; btn.innerHTML = ' Traitement...'; try { const resp = await fetch('api_v1_accept_rules.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: window.activeChannelId }) }); const data = await resp.json(); if (data.success) { const container = document.getElementById('rules-acceptance-container'); container.innerHTML = '
    Vous avez acceptรฉ les rรจgles.
    '; // Reload roles in members list if possible, or just reload page setTimeout(() => location.reload(), 1500); } else { alert(data.error || 'Erreur lors de l\'acceptation'); btn.disabled = false; btn.innerHTML = ' J\'accepte les rรจgles'; } } catch (e) { console.error(e); btn.disabled = false; btn.innerHTML = ' J\'accepte les rรจgles'; } }); document.getElementById('withdraw-rules-btn')?.addEventListener('click', async () => { if (!confirm('รŠtes-vous sรปr de vouloir retirer votre acceptation des rรจgles ? Vous perdrez le rรดle associรฉ.')) return; const btn = document.getElementById('withdraw-rules-btn'); btn.disabled = true; btn.innerHTML = ' Traitement...'; try { const resp = await fetch('api_v1_withdraw_rules.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: window.activeChannelId }) }); const data = await resp.json(); if (data.success) { location.reload(); } else { alert(data.error || 'Erreur lors du retrait'); btn.disabled = false; btn.innerHTML = ' Retirer mon acceptation'; } } catch (e) { console.error(e); btn.disabled = false; btn.innerHTML = ' Retirer mon acceptation'; } }); // Channel Selection Type const addChannelBtns = document.querySelectorAll('.add-channel-btn'); addChannelBtns.forEach(btn => { btn.addEventListener('click', () => { const type = btn.dataset.type; const select = document.getElementById('add-channel-type'); // Corrected ID from index.php if (select) { select.value = type === 'voice' ? 'voice' : (type || 'chat'); // Trigger change to update visibility select.dispatchEvent(new Event('change')); } }); }); const addChannelTypeSelect = document.getElementById('add-channel-type'); addChannelTypeSelect?.addEventListener('change', (e) => { const type = e.target.value; const container = document.getElementById('add-channel-rules-role-container'); if (container) { container.style.display = (type === 'rules') ? 'block' : 'none'; } const limitContainer = document.getElementById('add-channel-limit-container'); const filesContainer = document.getElementById('add-channel-files-container'); if (limitContainer) limitContainer.style.display = (type === 'rules' || type === 'autorole') ? 'none' : 'block'; if (filesContainer) filesContainer.style.display = (type === 'rules' || type === 'autorole') ? 'none' : 'block'; }); // User Settings - Avatar Search // User Settings - Save logic removed and moved to index.php for reliability const avatarSearchBtn = document.getElementById('search-avatar-btn'); const avatarRefreshBtn = document.getElementById('refresh-avatar-btn'); const avatarSearchQuery = document.getElementById('avatar-search-query'); const avatarResults = document.getElementById('avatar-results'); const avatarPreview = document.getElementById('settings-avatar-preview'); const avatarUrlInput = document.getElementById('settings-avatar-url'); const avatarUploadInput = document.getElementById('avatar-upload-input'); avatarUploadInput?.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('avatar', file); try { avatarPreview.innerHTML = '
    '; const resp = await fetch('api/upload_avatar.php', { method: 'POST', body: formData }); const data = await resp.json(); avatarPreview.innerHTML = ''; if (data.success) { avatarUrlInput.value = data.url; avatarPreview.style.backgroundImage = `url('${data.url}')`; } else { alert(data.error || 'Erreur lors de l\'upload'); } } catch (err) { console.error(err); avatarPreview.innerHTML = ''; alert('Erreur rรฉseau lors de l\'upload'); } }); const serverIconUploadInput = document.getElementById('server-icon-upload-input'); // serverIconPreview and serverIconUrlInput are already declared above serverIconUploadInput?.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('icon', file); formData.append('server_id', window.activeServerId); try { serverIconPreview.innerHTML = '
    '; const resp = await fetch('api/upload_server_icon.php', { method: 'POST', body: formData }); const data = await resp.json(); serverIconPreview.innerHTML = ''; if (data.success) { serverIconUrlInput.value = data.url; serverIconPreview.style.backgroundImage = `url('${data.url}')`; } else { alert(data.error || 'Erreur lors de l\'upload'); } } catch (err) { console.error(err); serverIconPreview.innerHTML = ''; alert('Erreur rรฉseau lors de l\'upload'); } }); let currentAvatarPage = 1; async function fetchAvatars(q, page = 1) { if (!q) return; avatarResults.innerHTML = '
    Searching...
    '; try { const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(q)}&page=${page}`); const data = await resp.json(); avatarResults.innerHTML = ''; if (data && Array.isArray(data)) { data.forEach(photo => { const img = document.createElement('img'); img.src = photo.url; img.className = 'avatar-pick'; img.style.width = '100%'; img.style.height = 'auto'; img.style.aspectRatio = '1/1'; img.style.objectFit = 'cover'; img.style.borderRadius = '4px'; img.style.cursor = 'pointer'; img.onclick = () => { avatarUrlInput.value = photo.url; avatarPreview.style.backgroundImage = `url('${photo.url}')`; }; avatarResults.appendChild(img); }); } else { avatarResults.innerHTML = '
    Aucun rรฉsultat trouvรฉ.
    '; } } catch (e) { console.error(e); avatarResults.innerHTML = '
    Erreur lors de la rรฉcupรฉration.
    '; } } avatarSearchBtn?.addEventListener('click', () => { currentAvatarPage = 1; fetchAvatars(avatarSearchQuery.value.trim(), currentAvatarPage); }); avatarRefreshBtn?.addEventListener('click', () => { currentAvatarPage++; fetchAvatars(avatarSearchQuery.value.trim() || 'avatar', currentAvatarPage); }); avatarSearchQuery?.addEventListener('keypress', (e) => { if (e.key === 'Enter') { currentAvatarPage = 1; fetchAvatars(avatarSearchQuery.value.trim(), currentAvatarPage); } }); // Theme preview document.querySelectorAll('input[name="theme"]').forEach(radio => { radio.addEventListener('change', (e) => { document.body.setAttribute('data-theme', e.target.value); }); }); // Toggle members sidebar const toggleMembersBtn = document.getElementById('toggle-members-btn'); const membersSidebar = document.querySelector('.members-sidebar'); if (toggleMembersBtn && membersSidebar) { toggleMembersBtn.addEventListener('click', () => { if (window.innerWidth > 992) { membersSidebar.classList.toggle('hidden'); } else { membersSidebar.classList.toggle('show'); } }); } // User Settings - Save handled in index.php function escapeHTML(str) { if (!str) return ""; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function parseMarkdown(text) { if (!text) return ""; // Escape HTML first let html = escapeHTML(text); // Code blocks: ```language\ncontent``` const codeBlocks = []; html = html.replace(/```(?:(\w+)\n)?([\s\S]*?)```/g, (match, lang, content) => { const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`; codeBlocks.push(`
    ${content}
    `); return placeholder; }); // Inline code: `content` const inlineCodes = []; html = html.replace(/`([^`\n]+)`/g, (match, content) => { const placeholder = `__INLINE_CODE_${inlineCodes.length}__`; inlineCodes.push(`${content}`); return placeholder; }); // Bold: **text** html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); // Italics: *text* or _text_ html = html.replace(/\*([^*]+)\*/g, '$1'); html = html.replace(/_([^_]+)_/g, '$1'); // Underline: __text__ html = html.replace(/__([^_]+)__/g, '$1'); // Strikethrough: ~~text~~ html = html.replace(/~~([^~]+)~~/g, '$1'); // Spoiler: ||text|| html = html.replace(/\|\|([^|]+)\|\|/g, '$1'); // Headers: # H1, ## H2, ### H3 (must be at start of line) html = html.replace(/^# (.*$)/gm, '

    $1

    '); html = html.replace(/^## (.*$)/gm, '

    $1

    '); html = html.replace(/^### (.*$)/gm, '

    $1

    '); // Subtext: -# text (must be at start of line) html = html.replace(/^-# (.*$)/gm, '$1'); // Blockquotes: > text or >>> text (must be at start of line) html = html.replace(/^> (.*$)/gm, '
    $1
    '); html = html.replace(/^>>> ([\s\S]*$)/g, '
    $1
    '); // Hyperlinks: [text](url) html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Pure links: html = html.replace(/<(https?:\/\/[^&]+)>/g, '$1'); // Newlines to
    (only those not inside placeholders) html = html.replace(/\n/g, '
    '); // Remove extra space around headers and blockquotes added by nl2br html = html.replace(/(
    )\s*(

    |

    |

    |
    )/gi, '$2'); html = html.replace(/(<\/h1>|<\/h2>|<\/h3>|<\/blockquote>)\s*(
    )/gi, '$1'); // Re-insert inline code inlineCodes.forEach((code, i) => { html = html.replace(`__INLINE_CODE_${i}__`, code); }); // Re-insert code blocks codeBlocks.forEach((block, i) => { html = html.replace(`__CODE_BLOCK_${i}__`, block); }); return html; } function parseCustomEmotes(text) { let parsed = parseMarkdown(text); (window.CUSTOM_EMOTES_CACHE || []).forEach(emote => { const imgHtml = `${emote.name}`; // Only replace if it's not inside a tag attribute or code block (simplified) parsed = parsed.split(emote.code).join(imgHtml); }); return parsed; } function appendMessage(msg) { if (!msg || !msg.id) return; if (document.querySelector(`.message-item[data-id="${msg.id}"]`)) return; // Security: Ensure message belongs to current channel/thread if (msg.channel_id && msg.channel_id != currentChannel) return; if (window.activeChannelType === 'forum') { if (!currentThread || msg.thread_id != currentThread) return; } else { if (msg.thread_id) return; } // Auto-populate metadata for video platforms if missing const dmRegexForMeta = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/; const dmMatchForMeta = msg.content.match(dmRegexForMeta); if (dmMatchForMeta && !msg.metadata) { msg.metadata = { title: 'Dailymotion Video', url: dmMatchForMeta[0], image: `https://www.dailymotion.com/thumbnail/video/${dmMatchForMeta[1]}`, site_name: 'Dailymotion' }; } const messagesList = document.getElementById('messages-list'); const messagesInner = document.querySelector('.messages-list-inner'); const targetContainer = messagesInner || messagesList; if (!targetContainer) return; const div = document.createElement('div'); div.className = 'message-item'; div.dataset.id = msg.id; div.dataset.rawContent = msg.content; if (parseInt(msg.id) > lastMessageId) { lastMessageId = parseInt(msg.id); } const avatarStyle = msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : ''; let attachmentHtml = ''; if (msg.attachment_url) { const ext = msg.attachment_url.split('.').pop().toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) { attachmentHtml = `
    Attachment
    `; } else { attachmentHtml = ``; } } let embedHtml = ''; if (msg.metadata) { const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata; embedHtml = `
    ${meta.site_name ? `
    ${escapeHTML(meta.site_name)}
    ` : ''} ${meta.title ? `${escapeHTML(meta.title)}` : ''} ${meta.description ? `
    ${escapeHTML(meta.description)}
    ` : ''} ${meta.image ? `
    ` : ''}
    `; } const isMe = msg.user_id == window.currentUserId || msg.username == window.currentUsername; const hasManageRights = window.canManageChannels || window.isServerOwner || false; const pinHtml = ` `; const actionsHtml = (isMe || hasManageRights) ? `
    ${pinHtml} ${isMe ? ` ` : ''}
    ` : ''; const pinnedBadge = msg.is_pinned ? ` Pinned ` : ''; const mentionRegex = new RegExp(`@${window.currentUsername}\\b`, 'g'); const mentionHtml = `@${window.currentUsername}`; const contentWithMentions = parseCustomEmotes(msg.content).replace(mentionRegex, mentionHtml); div.innerHTML = `
    ${escapeHTML(msg.username)} ${renderRoleIconJS(msg.role_icon, '14px')} ${msg.timestamp || 'Just now'} ${pinnedBadge}
    ${contentWithMentions}
    ${attachmentHtml} ${embedHtml}
    ${actionsHtml}
    `; targetContainer.appendChild(div); scrollToBottom(isMe); // Ensure we scroll again when images/videos load div.querySelectorAll('img, iframe').forEach(el => { el.addEventListener('load', () => scrollToBottom(isMe)); }); } // Initial load of roles for the server loadRoles(); // Autorole Toggle document.addEventListener('click', async (e) => { const btn = e.target.closest('.autorole-toggle-btn'); if (!btn) return; const roleId = btn.dataset.roleId; btn.disabled = true; try { const resp = await fetch('api_v1_autoroles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'toggle', role_id: roleId }) }); const data = await resp.json(); if (data.success) { if (data.added) { btn.classList.remove('btn-outline-secondary'); btn.classList.add('btn-primary'); btn.style.backgroundColor = 'var(--blurple)'; btn.style.border = 'none'; } else { btn.classList.add('btn-outline-secondary'); btn.classList.remove('btn-primary'); btn.style.backgroundColor = 'var(--bg-channels)'; btn.style.border = '1px solid var(--separator)'; } // Refresh sidebar channels and members if (typeof loadRoles === 'function') loadRoles(); fetch(window.location.href) .then(resp => resp.text()) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const newSidebar = doc.getElementById('sidebar-channels-list'); const currentSidebar = document.getElementById('sidebar-channels-list'); if (newSidebar && currentSidebar) { currentSidebar.innerHTML = newSidebar.innerHTML; if (window.restoreCollapsedStates) window.restoreCollapsedStates(); } }) .catch(err => console.error('Error refreshing sidebar:', err)); } else { alert(data.error || 'Failed to toggle role'); } } catch (e) { console.error(e); } finally { btn.disabled = false; } }); // Category Collapsible Logic document.addEventListener('click', (e) => { const categoryHeader = e.target.closest('.channel-category'); if (categoryHeader) { // Check if we didn't click on a settings button or add button if (e.target.closest('.channel-settings-btn') || e.target.closest('.add-channel-btn')) { return; } const wrapper = categoryHeader.closest('.category-wrapper'); if (wrapper) { wrapper.classList.toggle('collapsed'); // Persist state in localStorage const categoryId = wrapper.dataset.id; const collapsedStates = JSON.parse(localStorage.getItem('categoryCollapsedStates') || '{}'); collapsedStates[categoryId] = wrapper.classList.contains('collapsed'); localStorage.setItem('categoryCollapsedStates', JSON.stringify(collapsedStates)); } } }); // Restore collapsed states window.restoreCollapsedStates = () => { const collapsedStates = JSON.parse(localStorage.getItem('categoryCollapsedStates') || '{}'); Object.entries(collapsedStates).forEach(([id, isCollapsed]) => { if (isCollapsed) { const wrapper = document.querySelector(`.category-wrapper[data-id="${id}"]`); if (wrapper) { wrapper.classList.add('collapsed'); } } }); }; restoreCollapsedStates(); // Invite code refresh and timer const refreshBtn = document.getElementById('refresh-invite-code-btn'); const inviteInput = document.getElementById('server-invite-code'); const timerContainer = document.getElementById('invite-code-timer'); if (refreshBtn) { refreshBtn.addEventListener('click', async () => { const formData = new FormData(); formData.append('server_id', window.activeServerId); try { const resp = await fetch('api/refresh_invite_code.php', { method: 'POST', body: formData }); const data = await resp.json(); if (data.success) { if (inviteInput) inviteInput.value = data.invite_code; if (timerContainer) { timerContainer.dataset.expires = data.expires_at; timerContainer.innerHTML = 'Expires in: 30:00'; } } else { alert('Error: ' + data.error); } } catch (e) { console.error(e); alert('Failed to refresh invite code.'); } }); } function updateInviteTimer() { const display = document.getElementById('invite-timer-display'); const container = document.getElementById('invite-code-timer'); if (!display || !container || !container.dataset.expires) return; const expiresAt = new Date(container.dataset.expires).getTime(); const now = new Date().getTime(); const diff = expiresAt - now; if (diff <= 0) { container.innerHTML = 'Expired'; return; } const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); display.innerText = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } if (timerContainer) { setInterval(updateInviteTimer, 1000); updateInviteTimer(); } });