document.addEventListener('DOMContentLoaded', () => { const fileUpload = document.getElementById('file-upload'); const chatForm = document.getElementById('chat-form'); const chatInput = document.getElementById('chat-input'); const messagesList = document.getElementById('messages-list'); const typingIndicator = document.getElementById('typing-indicator'); function scrollToBottom(force = false) { if (!messagesList) return; // Smart scroll: only scroll if user is already at the bottom or if forced (e.g. sending a message) const threshold = 150; // pixels margin const isAtBottom = messagesList.scrollHeight - messagesList.scrollTop <= messagesList.clientHeight + threshold; if (force || isAtBottom) { messagesList.scrollTo({ top: messagesList.scrollHeight, behavior: 'smooth' }); // Backup for non-smooth support or rendering delays setTimeout(() => { if (force || messagesList.scrollHeight - messagesList.scrollTop <= messagesList.clientHeight + threshold + 200) { messagesList.scrollTop = messagesList.scrollHeight; } }, 100); } } // Emoji list categorized const EMOJI_CATEGORIES = { 'Smileys': ['๐Ÿ˜€', '๐Ÿ˜ƒ', '๐Ÿ˜„', '๐Ÿ˜', '๐Ÿ˜†', '๐Ÿ˜…', '๐Ÿคฃ', '๐Ÿ˜‚', '๐Ÿ™‚', '๐Ÿ™ƒ', '๐Ÿ˜‰', '๐Ÿ˜Š', '๐Ÿ˜‡', '๐Ÿฅฐ', '๐Ÿ˜', '๐Ÿคฉ', '๐Ÿ˜˜', '๐Ÿ˜—', '๐Ÿ˜š', '๐Ÿ˜™', '๐Ÿ˜‹', '๐Ÿ˜›', '๐Ÿ˜œ', '๐Ÿคช', '๐Ÿ˜', '๐Ÿค‘', '๐Ÿค—', '๐Ÿคญ', '๐Ÿคซ', '๐Ÿค”', '๐Ÿค', '๐Ÿคจ', '๐Ÿ˜', '๐Ÿ˜‘', '๐Ÿ˜ถ', '๐Ÿ˜', '๐Ÿ˜’', '๐Ÿ™„', '๐Ÿ˜ฌ', '๐Ÿคฅ', '๐Ÿ˜Œ', '๐Ÿ˜”', '๐Ÿ˜ช', '๐Ÿคค', '๐Ÿ˜ด', '๐Ÿ˜ท', '๐Ÿค’', '๐Ÿค•', '๐Ÿคข', '๐Ÿคฎ', '๐Ÿคง', '๐Ÿฅต', '๐Ÿฅถ', '๐Ÿฅด', '๐Ÿ˜ต', '๐Ÿคฏ', '๐Ÿค ', '๐Ÿฅณ', '๐Ÿ˜Ž', '๐Ÿค“', '๐Ÿง', '๐Ÿ˜•', '๐Ÿ˜Ÿ', '๐Ÿ™', 'โ˜น๏ธ', '๐Ÿ˜ฎ', '๐Ÿ˜ฏ', '๐Ÿ˜ฒ', '๐Ÿ˜ณ', '๐Ÿฅบ', '๐Ÿ˜ฆ', '๐Ÿ˜ง', '๐Ÿ˜จ', '๐Ÿ˜ฐ', '๐Ÿ˜ฅ', '๐Ÿ˜ข', '๐Ÿ˜ญ', '๐Ÿ˜ฑ', '๐Ÿ˜–', '๐Ÿ˜ฃ', '๐Ÿ˜ž', '๐Ÿ˜“', '๐Ÿ˜ฉ', '๐Ÿ˜ซ', '๐Ÿฅฑ', '๐Ÿ˜ค', '๐Ÿ˜ก', '๐Ÿ˜ ', '๐Ÿคฌ', '๐Ÿ˜ˆ', '๐Ÿ‘ฟ', '๐Ÿ‘น', '๐Ÿ‘บ', '๐Ÿ’€', 'โ˜ ๏ธ', '๐Ÿ’ฉ', '๐Ÿคก', '๐Ÿ‘น', '๐Ÿ‘บ', '๐Ÿ‘ป', '๐Ÿ‘ฝ', '๐Ÿ‘พ', '๐Ÿค–', '๐Ÿ˜บ', '๐Ÿ˜ธ', '๐Ÿ˜ป', '๐Ÿ˜ผ', '๐Ÿ˜ฝ', '๐Ÿ™€', '๐Ÿ˜ฟ', '๐Ÿ˜พ'], 'People': ['๐Ÿ‘‹', '๐Ÿคš', '๐Ÿ–๏ธ', 'โœ‹', '๐Ÿ––', '๐Ÿ‘Œ', '๐Ÿค', 'โœŒ๏ธ', '๐Ÿคž', '๐ŸคŸ', '๐Ÿค˜', '๐Ÿค™', '๐Ÿ‘ˆ', '๐Ÿ‘‰', '๐Ÿ‘†', '๐Ÿ–•', '๐Ÿ‘‡', 'โ˜๏ธ', '๐Ÿ‘', '๐Ÿ‘Ž', 'โœŠ', '๐Ÿ‘Š', '๐Ÿค›', '๐Ÿคœ', '๐Ÿ‘', '๐Ÿ™Œ', '๐Ÿ‘', '๐Ÿคฒ', '๐Ÿค', '๐Ÿ™', 'โœ๏ธ', '๐Ÿ’…', '๐Ÿคณ', '๐Ÿ’ช', '๐Ÿฆพ', '๐Ÿฆต', '๐Ÿฆฟ', '๐Ÿฆถ', '๐Ÿ‘‚', '๐Ÿฆป', '๐Ÿ‘ƒ', '๐Ÿง ', '๐Ÿฆท', '๐Ÿฆด', '๐Ÿ‘€', '๐Ÿ‘๏ธ', '๐Ÿ‘…', '๐Ÿ‘„', '๐Ÿ‘ถ', '๐Ÿง’', '๐Ÿ‘ฆ', '๐Ÿ‘ง', '๐Ÿง‘', '๐Ÿ‘ฑ', '๐Ÿ‘จ', '๐Ÿง”', '๐Ÿ‘ฉ', '๐Ÿง“', '๐Ÿ‘ด', '๐Ÿ‘ต', '๐Ÿ‘ฎ', '๐Ÿ•ต๏ธ', '๐Ÿ’‚', '๐Ÿ‘ท', '๐Ÿคด', '๐Ÿ‘ธ', '๐Ÿ‘ณ', '๐Ÿ‘ฒ', '๐Ÿง•', '๐Ÿคต', '๐Ÿ‘ฐ', '๐Ÿคฐ', '๐Ÿคฑ', '๐Ÿ‘ผ', '๐ŸŽ…', '๐Ÿคถ', '๐Ÿฆธ', '๐Ÿฆน', '๐Ÿง™', '๐Ÿงš', '๐Ÿง›', '๐Ÿงœ', '๐Ÿง', '๐Ÿงž', '๐ŸงŸ', '๐Ÿ’†', '๐Ÿ’‡', '๐Ÿšถ', '๐Ÿƒ', '๐Ÿ’ƒ', '๐Ÿ•บ', '๐Ÿ•ด๏ธ', '๐Ÿ‘ฏ', '๐Ÿง–', '๐Ÿง—'], 'Animals': ['๐Ÿถ', '๐Ÿฑ', '๐Ÿญ', '๐Ÿน', '๐Ÿฐ', '๐ŸฆŠ', '๐Ÿป', '๐Ÿผ', '๐Ÿจ', '๐Ÿฏ', '๐Ÿฆ', '๐Ÿฎ', '๐Ÿท', '๐Ÿฝ', '๐Ÿธ', '๐Ÿต', '๐Ÿ™ˆ', '๐Ÿ™‰', '๐Ÿ™Š', '๐Ÿ’', '๐Ÿ”', '๐Ÿง', '๐Ÿฆ', '๐Ÿค', '๐Ÿฃ', '๐Ÿฅ', '๐Ÿฆ†', '๐Ÿฆ…', '๐Ÿฆ‰', '๐Ÿฆ‡', '๐Ÿบ', '๐Ÿ—', '๐Ÿด', '๐Ÿฆ„', '๐Ÿ', '๐Ÿ›', '๐Ÿฆ‹', '๐ŸŒ', '๐Ÿž', '๐Ÿœ', '๐ŸฆŸ', '๐Ÿฆ—', '๐Ÿ•ท๏ธ', '๐Ÿ•ธ๏ธ', '่ ', '๐Ÿข', '๐Ÿ', '๐ŸฆŽ', '๐Ÿฆ–', '๐Ÿฆ•', '๐Ÿ™', '๐Ÿฆ‘', '๐Ÿฆ', '๐Ÿฆž', '๐Ÿฆ€', '๐Ÿก', '๐Ÿ ', '๐ŸŸ', '๐Ÿฌ', '๐Ÿณ', '๐Ÿ‹', '๐Ÿฆˆ', '๐ŸŠ', '๐Ÿ…', '๐Ÿ†', '๐Ÿฆ“', '๐Ÿฆ', '๐Ÿฆง', '๐Ÿ˜', '๐Ÿฆ›', '๐Ÿฆ', '๐Ÿช', '๐Ÿซ', '๐Ÿฆ’', '๐Ÿฆ˜', '๐Ÿƒ', '๐Ÿ‚', '๐Ÿ„', '๐ŸŽ', '๐Ÿ–', '๐Ÿ', '๐Ÿ‘', '๐Ÿ', '๐ŸฆŒ', '๐Ÿ•', '๐Ÿฉ', '๐Ÿฆฎ', '๐Ÿˆ', '๐Ÿ“', '๐Ÿฆƒ', '๐Ÿฆš', '๐Ÿฆœ', '๐Ÿฆข', '๐Ÿฆฉ', '๐Ÿ•Š๏ธ', '๐Ÿ‡', '๐Ÿฆ', '๐Ÿฆจ', '๐Ÿฆก', '๐Ÿฆฆ', '๐Ÿฆฅ', '๐Ÿ', '๐Ÿ€', '๐Ÿฟ๏ธ', '๐Ÿฆ”'], 'Nature': ['๐ŸŒต', '๐ŸŽ„', '๐ŸŒฒ', '๐ŸŒณ', '๐ŸŒด', '๐ŸŒฑ', '๐ŸŒฟ', 'โ˜˜๏ธ', '๐Ÿ€', '๐ŸŽ', '๐ŸŽ‹', '๐Ÿƒ', '๐Ÿ‚', '๐Ÿ', '๐Ÿ„', '๐Ÿš', '๐ŸŒพ', '๐Ÿ’', '๐ŸŒท', '๐ŸŒน', '๐Ÿฅ€', '๐ŸŒบ', '๐ŸŒธ', '๐ŸŒผ', '๐ŸŒป', '๐ŸŒž', '๐ŸŒ', '๐ŸŒ›', '๐ŸŒœ', '๐ŸŒš', '๐ŸŒ•', '๐ŸŒ–', '๐ŸŒ—', '๐ŸŒ˜', '๐ŸŒ‘', '๐ŸŒ’', '๐ŸŒ“', '๐ŸŒ”', '๐ŸŒ™', '๐ŸŒŽ', '๐ŸŒ', '๐ŸŒ', '๐Ÿช', '๐Ÿ’ซ', 'โญ๏ธ', '๐ŸŒŸ', 'โœจ', 'โšก๏ธ', 'โ˜„๏ธ', '๐Ÿ’ฅ', '๐Ÿ”ฅ', '๐ŸŒช๏ธ', '๐ŸŒˆ', 'โ˜€๏ธ', '๐ŸŒค๏ธ', 'โ›…๏ธ', '๐ŸŒฅ๏ธ', 'โ˜๏ธ', '๐ŸŒฆ๏ธ', '๐ŸŒง๏ธ', '๐ŸŒจ๏ธ', '๐ŸŒฉ๏ธ', 'โ„๏ธ', 'โ˜ƒ๏ธ', 'โ›„๏ธ', '๐ŸŒฌ๏ธ', '๐Ÿ’จ', '๐Ÿ’ง', '๐Ÿ’ฆ', 'โ˜”๏ธ', 'โ˜‚๏ธ', '๐ŸŒŠ', '๐ŸŒซ๏ธ'], 'Food': ['๐Ÿ', '๐ŸŽ', '๐Ÿ', '๐ŸŠ', '๐Ÿ‹', ' BANANA', '๐Ÿ‰', '๐Ÿ‡', '๐Ÿ“', '๐Ÿˆ', '๐Ÿ’', '๐Ÿ‘', '๐Ÿฅญ', '๐Ÿ', '๐Ÿฅฅ', '๐Ÿฅ', '๐Ÿ…', '๐Ÿ†', '๐Ÿฅ‘', '๐Ÿฅฆ', '๐Ÿฅฌ', '๐Ÿฅ’', '๐ŸŒฝ', '๐Ÿฅ•', '๐Ÿง„', '๐Ÿฅ”', '๐Ÿ ', '๐Ÿฅ', '๐Ÿฅฏ', '๐Ÿž', '๐Ÿฅ–', '๐Ÿฅจ', '๐Ÿง€', '๐Ÿฅš', '๐Ÿณ', '๐Ÿงˆ', '๐Ÿฅž', ' waffle', '๐Ÿฅ“', '๐Ÿฅฉ', '๐Ÿ—', '๐Ÿ–', '๐Ÿฆด', '๐ŸŒญ', '๐Ÿ”', '๐ŸŸ', '๐Ÿ•', '๐Ÿฅช', '๐Ÿฅ™', '๐Ÿง†', '๐ŸŒฎ', '๐ŸŒฏ', '๐Ÿฅ—', '๐Ÿฅ˜', '๐Ÿ', '๐Ÿœ', '๐Ÿฒ', '๐Ÿ›', ' sushi', ' Bento', ' Dumpling', ' Oyster', '๐Ÿค', ' Rice Ball', ' Rice', ' Rice Cracker', '๐Ÿฅ', '๐Ÿฅ ', '๐Ÿฅฎ', '๐Ÿข', '๐Ÿก', '๐Ÿง', '๐Ÿจ', '๐Ÿฆ', '๐Ÿฅง', '๐Ÿง', '๐Ÿฐ', '๐ŸŽ‚', '๐Ÿฎ', '๐Ÿญ', '๐Ÿฌ', '๐Ÿซ', ' popcorn', '๐Ÿฉ', '๐Ÿช', '๐ŸŒฐ', ' peanuts', '๐Ÿฏ', '๐Ÿฅ›', 'โ˜•๏ธ', '๐Ÿต', '๐Ÿฅค', '๐Ÿถ', '๐Ÿบ', '๐Ÿป', '๐Ÿฅ‚', '๐Ÿท', '๐Ÿฅƒ', '๐Ÿธ', '๐Ÿน', '๐Ÿง‰', '๐Ÿพ', '๐ŸงŠ', '๐Ÿฅ„', '๐Ÿด', '๐Ÿฝ๏ธ', '๐Ÿฅฃ', '๐Ÿฅก'], 'Travel': ['๐Ÿš—', '๐Ÿš•', '๐Ÿš™', '๐ŸšŒ', '๐ŸšŽ', '๐ŸŽ๏ธ', '๐Ÿš“', '๐Ÿš‘', '๐Ÿš’', '๐Ÿš', '๐Ÿšš', '๐Ÿš›', '๐Ÿšœ', '๐Ÿ›ต', '๐Ÿšฒ', '๐Ÿ›ด', '๐Ÿš', '๐Ÿ›ฃ๏ธ', '๐Ÿ›ค๏ธ', 'โ›ฝ๏ธ', '๐Ÿšจ', '๐Ÿšฅ', '๐Ÿšฆ', '๐Ÿšง', 'โš“๏ธ', 'โ›ต๏ธ', '๐Ÿšค', '๐Ÿ›ณ๏ธ', 'โ›ด๏ธ', '๐Ÿšข', 'โœˆ๏ธ', '๐Ÿ›ซ', '๐Ÿ›ฌ', '๐Ÿ’บ', '๐Ÿš', '๐ŸšŸ', '๐Ÿš ', '๐Ÿšก', '๐Ÿš€', '๐Ÿ›ธ', '๐Ÿ›ฐ๏ธ', 'โŒ›๏ธ', 'โณ', 'โŒš๏ธ', 'โฐ', 'โฑ๏ธ', 'โฒ๏ธ', '๐Ÿ•ฐ๏ธ', '๐ŸŒก๏ธ', 'โ˜€๏ธ', '๐Ÿช', '๐ŸŒŸ', 'โ˜๏ธ', 'โ›…๏ธ', 'โ›ˆ๏ธ', '๐ŸŒˆ', 'โ›ฐ๏ธ', '๐Ÿ”๏ธ', '๐Ÿ—ป', '๐ŸŒ‹', '๐Ÿœ๏ธ', '๐Ÿ•๏ธ', 'โ›บ๏ธ', '๐Ÿ ', '๐Ÿก', '๐Ÿข', '๐Ÿฃ', '๐Ÿค', '๐Ÿฅ', '๐Ÿฆ', '๐Ÿจ', '๐Ÿฉ', '๐Ÿช', '๐Ÿซ', '๐Ÿฌ', '๐Ÿญ', '๐Ÿฏ', '๐Ÿฐ', '๐Ÿ’’', '๐Ÿ—ผ', '๐Ÿ—ฝ', 'โ›ช๏ธ', '๐Ÿ•Œ', '๐Ÿ•', 'โ›ฉ๏ธ', '๐Ÿ•‹', 'โ›ฒ๏ธ', '๐ŸŒ', '๐ŸŒƒ', '๐Ÿ™๏ธ', '๐ŸŒ„', '๐ŸŒ…', '๐ŸŒ†', '๐ŸŒ‡', '๐ŸŒ‰', '๐ŸŽ ', '๐ŸŽก', '๐ŸŽข', '๐Ÿš‚', '๐Ÿšƒ', '๐Ÿš„', '๐Ÿš…', '๐Ÿš†', '๐Ÿš‡', '๐Ÿšˆ', '๐Ÿš‰', '๐ŸšŠ', '๐Ÿš', '๐Ÿšž', '๐Ÿš‹'], 'Activities': ['โšฝ๏ธ', '๐Ÿ€', '๐Ÿˆ', 'โšพ๏ธ', '๐ŸฅŽ', '๐ŸŽพ', '๐Ÿ', '๐Ÿ‰', '๐ŸŽฑ', '๐Ÿ“', '๐Ÿธ', '๐Ÿฅ…', ' hockey', ' field hockey', ' cricket', 'โ›ณ๏ธ', '๐Ÿน', '๐ŸŽฃ', ' boxing', '๐Ÿฅ‹', ' skateboard', '๐Ÿ›ท', 'โ›ธ๏ธ', '๐ŸฅŒ', '๐ŸŽฟ', 'โ›ท๏ธ', '๐Ÿ‚', '๐Ÿ‹๏ธ', ' fencing', '๐Ÿคผ', ' gymnastics', ' basketball player', '๐Ÿคฝ', ' handball', ' juggle', '๐Ÿง˜', '๐Ÿ‡', ' rowing', ' swimming', '๐Ÿšด', '๐Ÿšต', '๐Ÿง—', '๐ŸŽ–๏ธ', '๐Ÿ†', '๐Ÿ…', '๐Ÿฅ‡', '๐Ÿฅˆ', '๐Ÿฅ‰', '๐ŸŽซ', '๐ŸŽŸ๏ธ', '๐ŸŽญ', '๐ŸŽจ', '๐ŸŽฌ', '๐ŸŽค', '๐ŸŽง', '๐ŸŽผ', '๐ŸŽน', '๐Ÿฅ', '๐ŸŽท', '๐ŸŽบ', '๐ŸŽธ', '๐Ÿช•', '๐ŸŽป', '๐ŸŽฒ', 'โ™Ÿ๏ธ', '๐ŸŽฏ', '๐ŸŽณ', '๐ŸŽฎ', '๐ŸŽฐ', '๐Ÿงฉ'], 'Objects': ['โŒš๏ธ', '๐Ÿ“ฑ', '๐Ÿ“ฒ', '๐Ÿ’ป', 'โŒจ๏ธ', '๐Ÿ–ฑ๏ธ', '๐Ÿ–ฒ๏ธ', '๐Ÿ•น๏ธ', '๐Ÿ—œ๏ธ', '๐Ÿ’ฝ', '๐Ÿ’พ', '๐Ÿ’ฟ', '๐ŸŽž๏ธ', '๐Ÿ“ท', '๐Ÿ“ธ', '๐Ÿ“น', '๐Ÿ“ผ', '๐Ÿ”', '๐Ÿ”Ž', '๐Ÿ•ฏ๏ธ', '๐Ÿ’ก', '๐Ÿ”ฆ', '๐Ÿฎ', '๐Ÿช”', '๐Ÿ“”', '๐Ÿ“•', '๐Ÿ“–', '๐Ÿ“—', '๐Ÿ“˜', '๐Ÿ“™', '๐Ÿ“š', '๐Ÿ““', '๐Ÿ“’', '๐Ÿ“ƒ', '๐Ÿ“œ', '๐Ÿ“„', '๐Ÿ“ฐ', '๐Ÿ—ž๏ธ', '๐Ÿ“‘', '๐Ÿ”–', '๐Ÿท๏ธ', '๐Ÿ’ฐ', '๐Ÿ’ด', '๐Ÿ’ต', '๐Ÿ’ถ', '๐Ÿ’ท', '๐Ÿ’ธ', '๐Ÿ’ณ', '๐Ÿงพ', '๐Ÿ’น', 'โœ‰๏ธ', '๐Ÿ“ง', '๐Ÿ“จ', '๐Ÿ“ฉ', '๐Ÿ“ค', '๐Ÿ“ฅ', '๐Ÿ“ฆ', '๐Ÿ“ซ', '๐Ÿ“ฉ', '๐Ÿ“ฌ', '๐Ÿ“ญ', '๐Ÿ“ฎ', '๐Ÿ—ณ๏ธ', 'โœ๏ธ', 'โœ’๏ธ', '๐Ÿ–‹๏ธ', '๐Ÿ–Š๏ธ', '๐Ÿ–Œ๏ธ', '๐Ÿ–๏ธ', '๐Ÿ“', '๐Ÿ’ผ', '๐Ÿ“', '๐Ÿ“‚', '๐Ÿ—‚๏ธ', '๐Ÿ“…', '๐Ÿ“†', '๐Ÿ—’๏ธ', '๐Ÿ—“๏ธ', '๐Ÿ“‡', '๐Ÿ“ˆ', '๐Ÿ“‰', '๐Ÿ“Š', '๐Ÿ“‹', '๐Ÿ“Œ', '๐Ÿ“', '๐Ÿ“Ž', '๐Ÿ–‡๏ธ', '๐Ÿ“', '๐Ÿ“', 'โœ‚๏ธ', '๐Ÿ—ƒ๏ธ', '๐Ÿ—„๏ธ', '๐Ÿ—‘๏ธ', '๐Ÿ”’', '๐Ÿ”“', '๐Ÿ”', '๐Ÿ”', '๐Ÿ”‘', '๐Ÿ—๏ธ', '๐Ÿ”จ', '๐Ÿช“', 'โ›๏ธ', 'โš’๏ธ', '๐Ÿ› ๏ธ', '๐Ÿ—ก๏ธ', 'โš”๏ธ', '๐Ÿ”ซ', '๐Ÿ›ก๏ธ', '๐Ÿ”ง', '๐Ÿ”ฉ', 'โš™๏ธ', '๐Ÿ—œ๏ธ', 'โš–๏ธ', '๐Ÿฆฏ', '๐Ÿ”—', 'โ›“๏ธ', '๐Ÿงฐ', '๐Ÿงฒ', 'โš—๏ธ', '๐Ÿงช', '๐Ÿงซ', '๐Ÿงฌ', '๐Ÿ”ฌ', '๐Ÿ”ญ', '๐Ÿ“ก', '๐Ÿ’‰', '๐Ÿฉธ', '๐Ÿ’Š', '๐Ÿฉน', '๐Ÿฉบ', '๐Ÿšช', '๐Ÿ›๏ธ', '๐Ÿ›‹๏ธ', '๐Ÿช‘', '๐Ÿšฝ', '๐Ÿšฟ', '๐Ÿ›€', '๐Ÿช’', '๐Ÿงด', '๐Ÿงท', '๐Ÿงน', '๐Ÿงบ', '๐Ÿงป', '๐Ÿงผ', '๐Ÿงฏ', '๐Ÿ›’', '๐Ÿšฌ', 'โšฐ๏ธ', 'โšฑ๏ธ', '๐Ÿ—ฟ'], 'Symbols': ['๐Ÿ’˜', '๐Ÿ’', '๐Ÿ’–', '๐Ÿ’—', '๐Ÿ’“', '๐Ÿ’ž', '๐Ÿ’•', '๐Ÿ’Ÿ', 'โฃ๏ธ', '๐Ÿ’”', 'โค๏ธ', '๐Ÿงก', '๐Ÿ’›', '๐Ÿ’š', '๐Ÿ’™', '๐Ÿ’œ', '๐Ÿ–ค', '๐Ÿค', '๐ŸคŽ', '๐Ÿ’ฏ', '๐Ÿ’ข', '๐Ÿ’ฅ', '๐Ÿ’ซ', '๐Ÿ’ฆ', '๐Ÿ’จ', '๐Ÿ•ณ๏ธ', '๐Ÿ’ฃ', '๐Ÿ’ฌ', '๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ', '๐Ÿ—จ๏ธ', '๐Ÿ—ฏ๏ธ', '๐Ÿ’ญ', '๐Ÿ’ค', '๐ŸŒ', 'โ™ ๏ธ', 'โ™ฅ๏ธ', 'โ™ฆ๏ธ', 'โ™ฃ๏ธ', '๐Ÿƒ', '๐Ÿ€„๏ธ', '๐ŸŽด', '๐ŸŽญ', '๐Ÿ”‡', '๐Ÿ”ˆ', '๐Ÿ”‰', '๐Ÿ”Š', '๐Ÿ“ข', '๐Ÿ“ฃ', '๐Ÿ“ฏ', '๐Ÿ””', '๐Ÿ”•', '๐ŸŽผ', '๐ŸŽต', '๐ŸŽถ', '๐Ÿ’น', '๐Ÿง', '๐Ÿšฎ', '๐Ÿšฐ', 'โ™ฟ๏ธ', '๐Ÿšน', '๐Ÿšบ', '๐Ÿšป', '๐Ÿšผ', '๐Ÿšพ', '๐Ÿ›‚', '๐Ÿ›ƒ', '๐Ÿ›„', '๐Ÿ›…', 'โš ๏ธ', '๐Ÿšธ', 'โ›”๏ธ', '๐Ÿšซ', '๐Ÿšณ', '๐Ÿšญ', '๐Ÿšฏ', '๐Ÿšฑ', '๐Ÿšท', '๐Ÿ“ต', '๐Ÿ”ž', 'โ˜ข๏ธ', 'โ˜ฃ๏ธ', 'โฌ†๏ธ', 'โ†—๏ธ', 'โžก๏ธ', 'โ†˜๏ธ', 'โฌ‡๏ธ', 'โ†™๏ธ', 'โฌ…๏ธ', 'โ†–๏ธ', 'โ†•๏ธ', 'โ†”๏ธ', 'โ†ฉ๏ธ', 'โ†ช๏ธ', 'โคด๏ธ', 'โคต๏ธ', '๐Ÿ”ƒ', '๐Ÿ”„', '๐Ÿ”™', '๐Ÿ”š', '๐Ÿ”›', '๐Ÿ”œ', '๐Ÿ”', '๐Ÿ›', 'โš›๏ธ', '๐Ÿ•‰๏ธ', 'โœก๏ธ', 'โ˜ธ๏ธ', 'โ˜ฏ๏ธ', 'โœ๏ธ', 'โ˜ฆ๏ธ', 'โ˜ช๏ธ', 'โ˜ฎ๏ธ', '๐Ÿ•Ž', '๐Ÿ”ฏ', 'โ™ˆ๏ธ', 'โ™‰๏ธ', 'โ™Š๏ธ', 'โ™‹๏ธ', 'โ™Œ๏ธ', 'โ™๏ธ', 'โ™Ž๏ธ', 'โ™๏ธ', 'โ™๏ธ', 'โ™‘๏ธ', 'โ™’๏ธ', 'โ™“๏ธ', 'โ›Ž', '๐Ÿ”€', '๐Ÿ”', '๐Ÿ”‚', 'โ–ถ๏ธ', 'โฉ', 'โญ๏ธ', 'โฏ๏ธ', 'โ—€๏ธ', 'โช', 'โฎ๏ธ', '๐Ÿ”ผ', 'โซ', '๐Ÿ”ฝ', 'โฌ', 'โธ๏ธ', 'โน๏ธ', 'โบ๏ธ', 'โ๏ธ', '๐ŸŽฆ', '๐Ÿ”…', '๐Ÿ”†', '๐Ÿ“ถ', '๐Ÿ“ณ', '๐Ÿ“ด', 'โž•', 'โž–', 'โž—', 'โœ–๏ธ', 'โ™พ๏ธ', 'โ€ผ๏ธ', 'โ‰๏ธ', 'โ“', 'โ”', 'โ•', 'โ—๏ธ', 'ใ€ฐ๏ธ', '๐Ÿ’ฑ', '๐Ÿ’ฒ', 'โš•๏ธ', 'โ™ป๏ธ', 'โšœ๏ธ', '๐Ÿ”ฑ', '๐Ÿ“›', '๐Ÿ”ฐ', 'โญ•๏ธ', 'โœ…', 'โ˜‘๏ธ', 'โœ”๏ธ', 'โœ–๏ธ', 'โŒ', 'โŽ', 'โžฐ', 'โžฟ', 'ใ€ฝ๏ธ', 'โœณ๏ธ', 'โœด๏ธ', 'โ‡๏ธ', 'โ€ผ๏ธ', '๐Ÿˆ', '๐Ÿˆ‚๏ธ', '๐Ÿˆท๏ธ', '๐Ÿˆถ', '๐Ÿˆฏ๏ธ', '๐Ÿ‰', '๐Ÿˆน', '๐Ÿˆš๏ธ', '๐Ÿˆฒ', '๐Ÿ‰‘', '๐Ÿˆธ', '๐Ÿˆด', '๐Ÿˆณ', 'ใŠ—๏ธ', 'ใŠ™๏ธ', '๐Ÿˆบ', '๐Ÿˆต', '๐Ÿ”ด', '๐ŸŸ ', '๐ŸŸก', '๐ŸŸข', '๐Ÿ”ต', '๐ŸŸฃ', '๐ŸŸค', 'โšซ๏ธ', 'โšช๏ธ', '๐ŸŸฅ', '๐ŸŸง', '๐ŸŸจ', '๐ŸŸฉ', '๐ŸŸฆ', '๐ŸŸช', '๐ŸŸซ', 'โฌ›๏ธ', 'โฌœ๏ธ'], 'Flags': ['๐Ÿ', '๐Ÿšฉ', '๐ŸŽŒ', '๐Ÿด', '๐Ÿณ๏ธ', '๐Ÿณ๏ธโ€๐ŸŒˆ', '๐Ÿณ๏ธโ€โšง๏ธ', '๐Ÿดโ€โ˜ ๏ธ', '๐Ÿ‡ฆ๐Ÿ‡ซ', '๐Ÿ‡ฆ๐Ÿ‡ฝ', '๐Ÿ‡ฆ๐Ÿ‡ฑ', '๐Ÿ‡ฉ๐Ÿ‡ฟ', '๐Ÿ‡ฆ๐Ÿ‡ฒ', '๐Ÿ‡ฆ๐Ÿ‡บ', '๐Ÿ‡ฆ๐Ÿ‡น', '๐Ÿ‡ฆ๐Ÿ‡ฟ', '๐Ÿ‡ง๐Ÿ‡ช', '๐Ÿ‡ง๐Ÿ‡ท', '๐Ÿ‡จ๐Ÿ‡ฆ', '๐Ÿ‡จ๐Ÿ‡ฑ', '๐Ÿ‡จ๐Ÿ‡ณ', '๐Ÿ‡จ๐Ÿ‡ด', '๐Ÿ‡จ๐Ÿ‡ฟ', '๐Ÿ‡ฉ๐Ÿ‡ฐ', '๐Ÿ‡ช๐Ÿ‡ฌ', '๐Ÿ‡ซ๐Ÿ‡ฎ', '๐Ÿ‡ซ๐Ÿ‡ท', '๐Ÿ‡ฉ๐Ÿ‡ช', '๐Ÿ‡ฌ๐Ÿ‡ท', '๐Ÿ‡ญ๐Ÿ‡ฐ', '๐Ÿ‡ฎ๐Ÿ‡ณ', '๐Ÿ‡ฎ๐Ÿ‡ฉ', '๐Ÿ‡ฎ๐Ÿ‡ช', '๐Ÿ‡ฎ๐Ÿ‡ฑ', '๐Ÿ‡ฎ๐Ÿ‡น', '๐Ÿ‡ฏ๐Ÿ‡ต', '๐Ÿ‡ฐ๐Ÿ‡ท', '๐Ÿ‡ฒ๐Ÿ‡ฝ', '๐Ÿ‡ณ๐Ÿ‡ฑ', '๐Ÿ‡ณ๐Ÿ‡ฟ', '๐Ÿ‡ณ๐Ÿ‡ด', '๐Ÿ‡ต๐Ÿ‡ฐ', '๐Ÿ‡ต๐Ÿ‡ญ', '๐Ÿ‡ต๐Ÿ‡ฑ', '๐Ÿ‡ต๐Ÿ‡น', '๐Ÿ‡ท๐Ÿ‡บ', '๐Ÿ‡ธ๐Ÿ‡ฆ', '๐Ÿ‡ธ๐Ÿ‡ฌ', '๐Ÿ‡ฟ๐Ÿ‡ฆ', '๐Ÿ‡ช๐Ÿ‡ธ', '๐Ÿ‡ธ๐Ÿ‡ช', '๐Ÿ‡จ๐Ÿ‡ญ', '๐Ÿ‡น๐Ÿ‡ญ', '๐Ÿ‡น๐Ÿ‡ท', '๐Ÿ‡บ๐Ÿ‡ฆ', '๐Ÿ‡ฆ๐Ÿ‡ช', '๐Ÿ‡ฌ๐Ÿ‡ง', '๐Ÿ‡บ๐Ÿ‡ธ', '๐Ÿ‡ป๐Ÿ‡ณ'] }; // Global EMOJIS list for reactions (flattened) const EMOJIS = Object.values(EMOJI_CATEGORIES).flat(); function populateEmojiGrid(category = null, searchTerm = '') { const roleEmojiGrid = document.getElementById('role-emoji-grid'); if (!roleEmojiGrid) return; roleEmojiGrid.innerHTML = ''; let emojis = []; if (searchTerm) { emojis = EMOJIS.filter(e => e.includes(searchTerm) || searchTerm === ''); // For simple search by name, we'd need names, but since we only have the emoji characters, // the user might search by the emoji itself or we could just show all if search is empty. // Actually, without a mapping of emoji to names, searching is limited. // But I'll implement it anyway so if they paste an emoji it works, // or I can add common names if I had a mapping. // Given the constraint, I'll just filter by character for now, // or maybe the user just wants a way to filter the massive list. } else { emojis = EMOJI_CATEGORIES[category] || []; } emojis.forEach(emoji => { const span = document.createElement('span'); span.textContent = emoji; span.style.cursor = 'pointer'; span.style.fontSize = '20px'; span.style.padding = '5px'; span.style.textAlign = 'center'; span.className = 'rounded role-emoji-item'; span.onclick = () => { document.getElementById('edit-role-icon').value = emoji; document.getElementById('selected-role-emoji-preview').textContent = emoji; }; roleEmojiGrid.appendChild(span); }); } function initEmojiCategories() { const categoriesContainer = document.getElementById('role-emoji-categories'); const searchInput = document.getElementById('role-emoji-search'); if (!categoriesContainer) return; categoriesContainer.innerHTML = ''; Object.keys(EMOJI_CATEGORIES).forEach((cat, index) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-sm btn-dark text-nowrap px-2 py-1'; btn.style.fontSize = '0.75em'; btn.textContent = cat; if (index === 0) btn.classList.add('active', 'btn-primary'); btn.onclick = () => { if (searchInput) searchInput.value = ''; categoriesContainer.querySelectorAll('button').forEach(b => b.classList.remove('active', 'btn-primary')); btn.classList.add('active', 'btn-primary'); populateEmojiGrid(cat); }; categoriesContainer.appendChild(btn); }); if (searchInput) { searchInput.oninput = () => { const term = searchInput.value.trim(); if (term) { categoriesContainer.querySelectorAll('button').forEach(b => b.classList.remove('active', 'btn-primary')); populateEmojiGrid(null, term); } else { const activeCat = categoriesContainer.querySelector('button.active')?.textContent || Object.keys(EMOJI_CATEGORIES)[0]; populateEmojiGrid(activeCat); } }; } // Initial load populateEmojiGrid(Object.keys(EMOJI_CATEGORIES)[0]); } // Call init if elements exist if (document.getElementById('role-emoji-grid')) { initEmojiCategories(); } // Scroll to bottom scrollToBottom(true); const currentChannel = window.activeChannelId || new URLSearchParams(window.location.search).get('channel_id') || 1; const currentThread = new URLSearchParams(window.location.search).get('thread_id'); let typingTimeout; // Notification Permission if ("Notification" in window && Notification.permission === "default") { Notification.requestPermission(); } // WebSocket for real-time let ws; let voiceHandler; function connectWS() { try { ws = new WebSocket('ws://' + window.location.hostname + ':8080'); if (typeof VoiceChannel !== 'undefined') { voiceHandler = new VoiceChannel(ws); } ws.onmessage = (e) => { const msg = JSON.parse(e.data); // Voice signaling if (msg.type && msg.type.startsWith('voice_')) { if (voiceHandler) voiceHandler.handleSignaling(msg); return; } if (msg.type === 'message') { const data = JSON.parse(msg.data); if (data.channel_id == currentChannel) { appendMessage(data); // Desktop Notifications for mentions if (data.content.includes(`@${window.currentUsername}`) && data.user_id != window.currentUserId) { if (Notification.permission === "granted" && !window.isDndMode) { new Notification(`Mention in #${window.currentChannelName}`, { body: `${data.username}: ${data.content}`, icon: data.avatar_url || '' }); } } } } else if (msg.type === 'typing') { if (msg.channel_id == currentChannel && msg.user_id != window.currentUserId) { showTyping(msg.username); } } else if (msg.type === 'reaction') { updateReactionUI(msg.message_id, msg.reactions); } else if (msg.type === 'message_edit') { const el = document.querySelector(`.message-item[data-id="${msg.message_id}"] .message-text`); if (el) el.innerHTML = msg.content.replace(/\n/g, '
'); } else if (msg.type === 'message_delete') { document.querySelector(`.message-item[data-id="${msg.message_id}"]`)?.remove(); } else if (msg.type === 'presence') { updatePresenceUI(msg.user_id, msg.status); } }; ws.onopen = () => { ws.send(JSON.stringify({ type: 'presence', user_id: window.currentUserId, status: 'online' })); }; ws.onclose = () => setTimeout(connectWS, 3000); } catch (e) { console.warn('WebSocket connection failed.'); } } connectWS(); // Polling as fallback for real-time let lastMessageId = 0; const findLastMessageId = () => { const items = document.querySelectorAll('.message-item'); if (items.length > 0) { lastMessageId = Math.max(...Array.from(items).map(i => parseInt(i.dataset.id) || 0)); } }; findLastMessageId(); setInterval(async () => { if (!currentChannel) return; try { const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&after_id=${lastMessageId}`); const data = await resp.json(); if (data.success && data.messages && data.messages.length > 0) { data.messages.forEach(msg => { appendMessage(msg); }); } } catch (e) { } }, 1000); function showTyping(username) { if (!typingIndicator) return; typingIndicator.textContent = `${username} is typing...`; clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { if (typingIndicator) typingIndicator.textContent = ''; }, 3000); } chatInput?.addEventListener('input', () => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'typing', channel_id: currentChannel, user_id: window.currentUserId, username: window.currentUsername })); } }); chatForm?.addEventListener('submit', (e) => { e.preventDefault(); const content = chatInput.value.trim(); const file = fileUpload.files[0]; if (!content && !file) return; chatInput.value = ''; const formData = new FormData(); formData.append('content', content); formData.append('channel_id', currentChannel); if (currentThread) { formData.append('thread_id', currentThread); } const progressContainer = document.getElementById('upload-progress-container'); const progressBar = document.getElementById('upload-progress-bar'); const progressPercent = document.getElementById('upload-percentage'); const progressFilename = document.getElementById('upload-filename'); if (file) { formData.append('file', file); fileUpload.value = ''; // Clear file input // Show progress bar progressContainer.style.display = 'block'; progressFilename.textContent = `Uploading: ${file.name}`; progressBar.style.width = '0%'; progressPercent.textContent = '0%'; } const xhr = new XMLHttpRequest(); xhr.open('POST', 'api_v1_messages.php', true); xhr.upload.onprogress = (ev) => { if (ev.lengthComputable && file) { const percent = Math.round((ev.loaded / ev.total) * 100); progressBar.style.width = percent + '%'; progressPercent.textContent = percent + '%'; } }; xhr.onload = () => { if (xhr.status === 200) { const result = JSON.parse(xhr.responseText); if (result.success) { appendMessage(result.message); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'message', data: JSON.stringify({ ...result.message, channel_id: currentChannel }) })); } } else { alert(result.error || 'Failed to send message'); } } progressContainer.style.display = 'none'; }; xhr.onerror = () => { console.error('XHR Error'); progressContainer.style.display = 'none'; alert('An error occurred during the upload.'); }; xhr.send(formData); }); // Handle Reaction Clicks document.addEventListener('click', (e) => { const badge = e.target.closest('.reaction-badge'); if (badge) { const msgId = badge.parentElement.dataset.messageId; const emoji = badge.dataset.emoji; toggleReaction(msgId, emoji); return; } const addBtn = e.target.closest('.add-reaction-btn'); if (addBtn) { const msgId = addBtn.parentElement.dataset.messageId; showEmojiPicker(addBtn, (emoji) => toggleReaction(msgId, emoji)); return; } // Close picker if click outside if (!e.target.closest('.emoji-picker')) { const picker = document.querySelector('.emoji-picker'); if (picker) picker.remove(); } }); async function toggleReaction(messageId, emoji) { try { const resp = await fetch('api_v1_reactions.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message_id: messageId, emoji: emoji }) }); const result = await resp.json(); if (result.success) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'reaction', message_id: messageId, reactions: result.reactions })); } updateReactionUI(messageId, result.reactions); } } catch (e) { console.error(e); } } function showEmojiPicker(anchor, callback) { document.querySelector('.emoji-picker')?.remove(); const picker = document.createElement('div'); picker.className = 'emoji-picker p-0 overflow-hidden d-flex flex-column'; picker.style.width = '280px'; picker.style.height = '320px'; picker.style.backgroundColor = 'var(--bg-secondary)'; picker.style.border = '1px solid var(--bg-tertiary)'; picker.style.borderRadius = '8px'; picker.style.boxShadow = '0 8px 24px rgba(0,0,0,0.5)'; picker.style.zIndex = '2000'; const tabs = document.createElement('div'); tabs.className = 'd-flex overflow-auto border-bottom border-secondary p-1 bg-dark'; tabs.style.gap = '2px'; const searchContainer = document.createElement('div'); searchContainer.className = 'p-2 border-bottom border-secondary'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Search emoji...'; searchInput.className = 'form-control form-control-sm bg-dark border-secondary text-white'; searchContainer.appendChild(searchInput); const grid = document.createElement('div'); grid.className = 'flex-grow-1 overflow-auto p-2'; grid.style.display = 'grid'; grid.style.gridTemplateColumns = 'repeat(7, 1fr)'; grid.style.gap = '2px'; const renderGrid = (cat = null, term = '') => { grid.innerHTML = ''; let emojis = []; if (term) { emojis = EMOJIS.filter(e => e.includes(term)); } else { emojis = EMOJI_CATEGORIES[cat] || []; } emojis.forEach(emoji => { const span = document.createElement('span'); span.textContent = emoji; span.style.cursor = 'pointer'; span.style.fontSize = '20px'; span.style.padding = '5px'; span.style.textAlign = 'center'; span.className = 'rounded role-emoji-item'; span.onclick = () => { callback(emoji); picker.remove(); }; grid.appendChild(span); }); }; searchInput.oninput = () => { const term = searchInput.value.trim(); if (term) { tabs.querySelectorAll('button').forEach(b => b.classList.remove('text-primary')); renderGrid(null, term); } else { const activeCat = tabs.querySelector('button.text-primary')?.textContent || Object.keys(EMOJI_CATEGORIES)[0]; renderGrid(activeCat); } }; Object.keys(EMOJI_CATEGORIES).forEach((cat, index) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-sm btn-dark text-nowrap px-2 py-1 border-0'; btn.style.fontSize = '0.7em'; btn.textContent = cat; if (index === 0) btn.classList.add('text-primary'); btn.onclick = (e) => { e.stopPropagation(); searchInput.value = ''; tabs.querySelectorAll('button').forEach(b => b.classList.remove('text-primary')); btn.classList.add('text-primary'); renderGrid(cat); }; tabs.appendChild(btn); }); picker.appendChild(tabs); picker.appendChild(searchContainer); picker.appendChild(grid); document.body.appendChild(picker); renderGrid(Object.keys(EMOJI_CATEGORIES)[0]); const rect = anchor.getBoundingClientRect(); let top = rect.top - picker.offsetHeight - 5; if (top < 0) top = rect.bottom + 5; picker.style.position = 'fixed'; picker.style.top = `${top}px`; picker.style.left = `${Math.min(rect.left, window.innerWidth - 300)}px`; } function updateReactionUI(messageId, reactions) { const container = document.querySelector(`.message-reactions[data-message-id="${messageId}"]`); if (!container) return; const addBtn = container.querySelector('.add-reaction-btn'); container.innerHTML = ''; reactions.forEach(r => { const badge = document.createElement('span'); const userList = r.users.split(','); const active = userList.includes(String(window.currentUserId)); badge.className = `reaction-badge ${active ? 'active' : ''}`; badge.dataset.emoji = r.emoji; badge.innerHTML = `${r.emoji} ${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'; } } // Voice if (voiceHandler) { document.querySelectorAll('.voice-item').forEach(item => { item.addEventListener('click', () => { const cid = item.dataset.channelId; if (voiceHandler.currentChannelId == cid) { voiceHandler.leave(); item.classList.remove('active'); } else { voiceHandler.join(cid); document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active')); item.classList.add('active'); } }); }); } // Message Actions (Edit/Delete) document.addEventListener('click', async (e) => { const editBtn = e.target.closest('.action-btn.edit'); if (editBtn) { const msgId = editBtn.dataset.id; const msgItem = editBtn.closest('.message-item'); const textEl = msgItem.querySelector('.message-text'); const originalContent = textEl.innerText; const input = document.createElement('input'); input.type = 'text'; input.className = 'form-control bg-dark text-white'; input.value = originalContent; textEl.innerHTML = ''; textEl.appendChild(input); input.focus(); input.onkeydown = async (ev) => { if (ev.key === 'Enter') { const newContent = input.value.trim(); if (newContent && newContent !== originalContent) { const resp = await fetch('api_v1_messages.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: msgId, content: newContent }) }); if ((await resp.json()).success) { textEl.innerHTML = newContent.replace(/\n/g, '
'); ws?.send(JSON.stringify({ type: 'message_edit', message_id: msgId, content: newContent })); } } else { textEl.innerHTML = originalContent.replace(/\n/g, '
'); } } else if (ev.key === 'Escape') { textEl.innerHTML = originalContent.replace(/\n/g, '
'); } }; 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.style.backgroundColor = 'transparent'; const authorStyle = msg.role_color ? `color: ${msg.role_color};` : ''; div.innerHTML = `
${escapeHTML(msg.username)} ${msg.role_icon ? `` : ''} ${msg.time}
${escapeHTML(msg.content).replace(/\n/g, '
')}
`; 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 = `
${res.username}
${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); }); 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 roles...
  • '; try { const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`); const data = await resp.json(); if (data.success) { addPermRoleList.innerHTML = ''; // Filter out roles already in overrides const existingRoleIds = channelPermissionsData.map(p => parseInt(p.role_id)); const availableRoles = data.roles.filter(role => !existingRoleIds.includes(parseInt(role.id))); if (availableRoles.length === 0) { addPermRoleList.innerHTML = '
  • No more roles to add
  • '; if (window.canManageServer) { const divider = document.createElement('li'); divider.innerHTML = ''; addPermRoleList.appendChild(divider); const createLink = document.createElement('li'); createLink.innerHTML = ' Create roles in Server Settings'; addPermRoleList.appendChild(createLink); } return; } // Add Roles section 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); selectOverrideRole(role.id, role.name); } else { alert("Error adding permission: " + (postData.error || "Unknown error")); } }; addPermRoleList.appendChild(li); }); } else { addPermRoleList.innerHTML = `
  • Error: ${data.error || 'Failed to load'}
  • `; } } catch (err) { addPermRoleList.innerHTML = '
  • Network error
  • '; console.error(err); } } function renderRoleOverridesList(channelId) { channelPermissionsRolesList.innerHTML = ''; if (channelPermissionsData.length === 0) { channelPermissionsRolesList.innerHTML = '
    No overrides configured for this channel.
    '; return; } // Sort: @everyone always at top, then by name const sortedData = [...channelPermissionsData].sort((a, b) => { const nameA = (a.role_name || '').toLowerCase(); const nameB = (b.role_name || '').toLowerCase(); const isAEveryone = nameA.includes('everyone'); const isBEveryone = nameB.includes('everyone'); if (isAEveryone && !isBEveryone) return -1; if (!isAEveryone && isBEveryone) return 1; return nameA.localeCompare(nameB); }); sortedData.forEach(p => { const item = document.createElement('div'); item.className = `list-group-item list-group-item-action bg-transparent text-white border-0 mb-1 p-2 small d-flex align-items-center ${currentSelectedOverrideRole == p.role_id ? 'active' : ''}`; item.style.cursor = 'pointer'; item.innerHTML = `
    ${p.role_name || 'Unknown Role'} `; item.onclick = () => selectOverrideRole(p.role_id, p.role_name || 'Unknown Role'); channelPermissionsRolesList.appendChild(item); }); } function selectOverrideRole(roleId, roleName) { currentSelectedOverrideRole = roleId; const channelId = document.getElementById('edit-channel-id').value; // Update list active state renderRoleOverridesList(channelId); selectedPermRoleName.textContent = roleName; noRoleSelectedView.classList.add('d-none'); channelPermissionsSettings.classList.remove('d-none'); // Load existing permissions for this role const p = channelPermissionsData.find(perm => perm.role_id == roleId) || { allow_permissions: 0, deny_permissions: 0 }; // Update toggles (for now only bit 1: View Channel) updateToggleUI(1, p.allow_permissions, p.deny_permissions); } function updateToggleUI(bit, allowPerms, denyPerms) { const group = document.querySelector(`.perm-tri-state[data-perm-bit="${bit}"]`); if (!group) return; if (allowPerms & bit) { group.querySelector('input[value="allow"]').checked = true; } else if (denyPerms & bit) { group.querySelector('input[value="deny"]').checked = true; } else { group.querySelector('input[value="neutral"]').checked = true; } } removeSelectedPermRole?.addEventListener('click', async () => { if (!currentSelectedOverrideRole) return; const channelId = document.getElementById('edit-channel-id').value; await fetch('api_v1_channel_permissions.php', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: channelId, role_id: currentSelectedOverrideRole }) }); currentSelectedOverrideRole = null; channelPermissionsSettings.classList.add('d-none'); noRoleSelectedView.classList.remove('d-none'); loadChannelPermissions(channelId); }); permissionsTogglesContainer?.addEventListener('change', async (e) => { if (e.target.type === 'radio') { const group = e.target.closest('.perm-tri-state'); const bit = parseInt(group.dataset.permBit); const val = e.target.value; const channelId = document.getElementById('edit-channel-id').value; const roleId = currentSelectedOverrideRole; let p = channelPermissionsData.find(perm => perm.role_id == roleId); if (!p) { p = { role_id: roleId, allow_permissions: 0, deny_permissions: 0 }; } let allow = parseInt(p.allow_permissions); let deny = parseInt(p.deny_permissions); // Clear current bit allow &= ~bit; deny &= ~bit; if (val === 'allow') allow |= bit; if (val === 'deny') deny |= bit; await fetch('api_v1_channel_permissions.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel_id: channelId, role_id: roleId, allow, deny }) }); // Update local data p.allow_permissions = allow; p.deny_permissions = deny; } }); document.addEventListener('click', async (e) => { if (!e.target.closest('.search-container')) { searchResults.style.display = 'none'; } if (e.target.classList.contains('move-rule-btn')) { if (!window.canManageChannels) return; const id = e.target.dataset.id; const dir = e.target.dataset.dir; const resp = await fetch('api_v1_rules.php', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, dir }) }); if ((await resp.json()).success) location.reload(); } }); // Roles Management const channelSettingsBtns = document.querySelectorAll('.channel-settings-btn'); channelSettingsBtns.forEach(btn => { btn.addEventListener('click', () => { const modal = document.getElementById('editChannelModal'); const channelId = btn.dataset.id; const channelType = btn.dataset.type || 'chat'; const channelName = btn.dataset.name; modal.querySelector('#edit-channel-id').value = channelId; modal.querySelector('#edit-channel-name').value = channelName; modal.querySelector('#header-channel-name').textContent = channelName; modal.querySelector('#edit-channel-type').value = channelType; // Force switch to Overview tab const overviewTabBtn = modal.querySelector('[data-bs-target="#edit-channel-general"]'); if (overviewTabBtn) { bootstrap.Tab.getOrCreateInstance(overviewTabBtn).show(); } modal.querySelector('#edit-channel-files').checked = btn.dataset.files == '1'; modal.querySelector('#edit-channel-limit').value = btn.dataset.limit || ''; modal.querySelector('#edit-channel-status').value = btn.dataset.status || ''; modal.querySelector('#edit-channel-icon').value = btn.dataset.icon || ''; modal.querySelector('#edit-channel-rules-role').value = btn.dataset.rulesRole || ''; modal.querySelector('#edit-channel-category-id').value = btn.dataset.category || ''; modal.querySelector('#delete-channel-id').value = channelId; // 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 const editLimitContainer = document.getElementById('edit-channel-limit-container'); const editFilesContainer = document.getElementById('edit-channel-files-container'); const clearChatBtn = document.getElementById('clear-channel-history-btn'); if (editLimitContainer) editLimitContainer.style.display = (channelType === 'rules') ? 'none' : 'block'; if (editFilesContainer) editFilesContainer.style.display = (channelType === 'rules') ? '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'); 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'); if (editLimitContainer) editLimitContainer.style.display = (type === 'rules') ? 'none' : 'block'; if (editFilesContainer) editFilesContainer.style.display = (type === 'rules') ? '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(); } }); // Clear Channel History const clearHistoryBtn = document.getElementById('clear-channel-history-btn'); clearHistoryBtn?.addEventListener('click', async () => { const channelId = document.getElementById('edit-channel-id').value; if (!confirm('Voulez-vous vraiment vider tout l\'historique de ce salon ? Cette action est irrรฉversible.')) return; try { const formData = new FormData(); formData.append('channel_id', channelId); const resp = await fetch('api_v1_clear_channel.php', { method: 'POST', body: formData }); const result = await resp.json(); if (result.success) { location.reload(); } else { alert(result.error || 'Erreur lors du nettoyage de l\'historique'); } } catch (e) { console.error(e); } }); // Roles Management const rolesTabBtn = document.getElementById('roles-tab-btn'); const rolesList = document.getElementById('roles-list'); const addRoleBtn = document.getElementById('add-role-btn'); const membersTabBtn = document.getElementById('members-tab-btn'); const membersList = document.getElementById('server-members-list'); const activeServerId = window.activeServerId || new URLSearchParams(window.location.search).get('server_id') || 1; let serverRoles = []; let serverPermissions = []; rolesTabBtn?.addEventListener('click', loadRoles); membersTabBtn?.addEventListener('click', loadRoles); // Both tabs need roles data async function loadRoles() { try { const channelIdParam = window.activeChannelId ? `&channel_id=${window.activeChannelId}` : ''; const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}${channelIdParam}`); const data = await resp.json(); if (data.success) { serverRoles = data.roles; serverPermissions = data.permissions_list; if (rolesList) renderRoles(data.roles); if (membersList) renderMembers(data.members); // Use filtered members for sidebar, all members for colors updateGlobalUI(data.filtered_members || data.members, data.members); } } catch (e) { console.error(e); } } function renderRoleIconJS(icon, size = '12px') { if (!icon) return ''; const isUrl = icon.startsWith('http') || icon.startsWith('/'); if (isUrl) { return ``; } 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, '12px'); 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, '12px'); if (newIconHtml) { if (iconEl) { const temp = document.createElement('div'); temp.innerHTML = newIconHtml; iconEl.replaceWith(temp.firstChild); } else { const temp = document.createElement('div'); temp.innerHTML = newIconHtml; // Insert after the text node authorEl.insertBefore(temp.firstChild, authorEl.childNodes[1]); } } else if (iconEl) { iconEl.remove(); } } }); } function renderRoles(roles) { rolesList.innerHTML = ''; if (roles.length === 0) { rolesList.innerHTML = '
    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 isUrl = role.icon_url && (role.icon_url.startsWith('http') || role.icon_url.startsWith('/')); item.innerHTML = `
    ${role.name} ${role.icon_url ? (isUrl ? `` : `${role.icon_url}`) : ''}
    `; rolesList.appendChild(item); }); // Initialize Sortable for roles if (typeof Sortable !== 'undefined' && rolesList) { new Sortable(rolesList, { animation: 150, handle: '.role-drag-handle', ghostClass: 'sortable-ghost', onEnd: () => saveRolePositions() }); } } async function saveRolePositions() { const orders = []; const items = rolesList.querySelectorAll('.role-sortable-item'); // Invert the order because we ORDER BY position DESC in SQL let position = items.length - 1; items.forEach(item => { orders.push({ id: item.dataset.id, position: position-- }); }); try { await fetch('api_v1_roles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'reorder', server_id: activeServerId, orders: orders }) }); } catch (e) { console.error(e); } } function renderMembers(members) { membersList.innerHTML = ''; members.forEach(member => { const item = document.createElement('div'); item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-2 rounded bg-dark'; const isIconUrl = member.role_icon && (member.role_icon.startsWith('http') || member.role_icon.startsWith('/')); const roleIconHtml = member.role_icon ? (isIconUrl ? `` : `${member.role_icon}`) : ''; 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 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 resp = await fetch('api_v1_roles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update', server_id: activeServerId, id, name, color, icon_url, permissions }) }); const data = await resp.json(); if (data.success) { btn.textContent = 'Saved โœ…'; btn.classList.replace('btn-primary', 'btn-success'); setTimeout(() => { btn.textContent = originalText; btn.classList.replace('btn-success', 'btn-primary'); }, 2000); loadRoles(); } } catch (e) { console.error(e); } }); async function openEditUserRolesModal(userId, username, avatar) { const modal = document.getElementById('editUserRolesModal'); document.getElementById('edit-user-roles-user-id').value = userId; document.getElementById('edit-user-roles-username').textContent = username; const avatarEl = document.getElementById('edit-user-roles-avatar'); avatarEl.style.backgroundImage = avatar ? `url('${avatar}')` : 'none'; const list = document.getElementById('user-roles-selection-list'); list.innerHTML = '
    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 () => { const name = prompt('Role name:'); if (!name) return; const color = '#99aab5'; try { const resp = await fetch('api_v1_roles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'create', server_id: activeServerId, name, color, permissions: 0 }) }); if ((await resp.json()).success) loadRoles(); } catch (e) { console.error(e); } }); rolesList?.addEventListener('click', async (e) => { if (e.target.classList.contains('delete-role-btn')) { if (!confirm('Delete this role?')) return; const roleId = e.target.dataset.id; const resp = await fetch('api_v1_roles.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', server_id: activeServerId, id: roleId }) }); if ((await resp.json()).success) loadRoles(); } }); // Webhooks Management const webhooksTabBtn = document.getElementById('webhooks-tab-btn'); const webhooksList = document.getElementById('webhooks-list'); const addWebhookBtn = document.getElementById('add-webhook-btn'); webhooksTabBtn?.addEventListener('click', loadWebhooks); async function loadWebhooks() { webhooksList.innerHTML = '
    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(); }); 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') ? 'none' : 'block'; if (filesContainer) filesContainer.style.display = (type === 'rules') ? 'none' : 'block'; }); // User Settings - Avatar Search const avatarSearchBtn = document.getElementById('search-avatar-btn'); const avatarSearchQuery = document.getElementById('avatar-search-query'); const avatarResults = document.getElementById('avatar-results'); const avatarPreview = document.getElementById('settings-avatar-preview'); const avatarUrlInput = document.getElementById('settings-avatar-url'); avatarSearchBtn?.addEventListener('click', async () => { const q = avatarSearchQuery.value.trim(); if (!q) return; avatarResults.innerHTML = '
    Searching...
    '; try { const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(q)}`); const data = await resp.json(); avatarResults.innerHTML = ''; data.forEach(photo => { const img = document.createElement('img'); img.src = photo.url; img.className = 'avatar-pick'; img.style.width = '60px'; img.style.height = '60px'; img.style.cursor = 'pointer'; img.onclick = () => { avatarUrlInput.value = photo.url; avatarPreview.style.backgroundImage = `url('${photo.url}')`; }; avatarResults.appendChild(img); }); } catch (e) { console.error(e); } }); // Toggle members sidebar const toggleMembersBtn = document.getElementById('toggle-members-btn'); const membersSidebar = document.querySelector('.members-sidebar'); if (toggleMembersBtn && membersSidebar) { toggleMembersBtn.addEventListener('click', () => { if (window.innerWidth > 992) { membersSidebar.classList.toggle('hidden'); } else { membersSidebar.classList.toggle('show'); } }); } // User Settings - Save const saveSettingsBtn = document.getElementById('save-settings-btn'); saveSettingsBtn?.addEventListener('click', async () => { const form = document.getElementById('user-settings-form'); const formData = new FormData(form); const dndMode = document.getElementById('dnd-switch').checked ? '1' : '0'; formData.append('dnd_mode', dndMode); const theme = form.querySelector('input[name="theme"]:checked').value; document.body.setAttribute('data-theme', theme); const resp = await fetch('api_v1_user.php', { method: 'POST', body: formData }); const result = await resp.json(); if (result.success) { location.reload(); } else { alert(result.error || 'Failed to save settings'); } }); function escapeHTML(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function appendMessage(msg) { if (!msg || !msg.id) return; if (document.querySelector(`.message-item[data-id="${msg.id}"]`)) return; // Auto-populate metadata for video platforms if missing const dmRegexForMeta = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/; const dmMatchForMeta = msg.content.match(dmRegexForMeta); if (dmMatchForMeta && !msg.metadata) { msg.metadata = { title: 'Dailymotion Video', url: dmMatchForMeta[0], image: `https://www.dailymotion.com/thumbnail/video/${dmMatchForMeta[1]}`, site_name: 'Dailymotion' }; } const messagesList = document.getElementById('messages-list'); const div = document.createElement('div'); div.className = 'message-item'; div.dataset.id = msg.id; if (parseInt(msg.id) > lastMessageId) { lastMessageId = parseInt(msg.id); } const avatarStyle = msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : ''; let attachmentHtml = ''; if (msg.attachment_url) { const ext = msg.attachment_url.split('.').pop().toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) { attachmentHtml = `
    Attachment
    `; } else { attachmentHtml = `
    ${msg.attachment_url.split('/').pop()}
    `; } } 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'); if (msg.content.match(mentionRegex)) { div.classList.add('mentioned'); } if (msg.is_pinned) div.classList.add('pinned'); const ytRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; const dmRegex = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/; const vimeoRegex = /(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/; const ytMatch = msg.content.match(ytRegex); const dmMatch = msg.content.match(dmRegex); const vimeoMatch = msg.content.match(vimeoRegex); let videoHtml = ''; if (ytMatch && ytMatch[1]) { videoHtml = `
    `; } else if (dmMatch && dmMatch[1]) { videoHtml = `
    `; } else if (vimeoMatch && vimeoMatch[1]) { videoHtml = `
    `; } const authorStyle = msg.role_color ? `color: ${msg.role_color};` : ''; const isRoleIconUrl = msg.role_icon && (msg.role_icon.startsWith('http') || msg.role_icon.startsWith('/')); const roleIcon = msg.role_icon ? (isRoleIconUrl ? `` : `${msg.role_icon}`) : ''; div.innerHTML = `
    ${escapeHTML(msg.username)} ${roleIcon} ${msg.time} ${pinnedBadge}
    ${escapeHTML(msg.content).replace(/\n/g, '
    ').replace(mentionRegex, `@${window.currentUsername}`)} ${attachmentHtml} ${videoHtml} ${embedHtml}
    +
    ${actionsHtml} `; messagesList.appendChild(div); scrollToBottom(isMe); // Ensure we scroll again when images/videos load div.querySelectorAll('img, iframe').forEach(el => { el.addEventListener('load', () => scrollToBottom(isMe)); }); } // Initial load of roles for the server loadRoles(); });