document.write(`
Forgotten password ?
Enter email
Register for new account
Forgotten password ?
Enter email
`); class Chat { constructor(room, username = '', avatar) { this.server = 'https://www.shoutbox.com'; this.AJAX = `${this.server}/chat/ajax.php`; this.myUser = { username, room, avatar: avatar || `${this.server}/avatars/${Math.ceil(Math.random() * 29)}.svg`, password: '', id: Date.now(), isAdmin: false }; this.users = {}; window.shoutbox = this; this.traductions = { welcome: "Welcome %s. ", userOnline: "%s user online", usersOnline: "%s users online", enterYourTextHere: "Enter your text here", serverMessage: "
%s
", enterAdminPassword: "Enter admin password", imageAvatar: "", youAreAdminNow: "You are admin now.", mp3: "https://www.shoutbox.com/chat/mp3/dink.mp3", addUser: "
%s %s
", banText: "", receivedText: "
%s%s %s: %s
", youBannedUser: "You banned %s" }; this.smileys = { ':)': '๐Ÿ™‚', ';)': '๐Ÿ™‚', ':D': '๐Ÿ˜ƒ', 'xD': '๐Ÿ˜†', ':(': '๐Ÿ˜Ÿ', ":'(": '๐Ÿ˜ข', '>:(': '๐Ÿ˜ ', ':O': '๐Ÿ˜ฎ', ':$': '๐Ÿ˜ณ', ':|': '๐Ÿ˜', '<3': 'โค' }; // quick sprintf this.sprintf = (str, ...argv) => !argv.length ? str : this.sprintf(str.replace(/%s/, argv.shift()), ...argv); // polyfill replaceAll if needed if (!String.prototype.replaceAll) { // eslint-disable-next-line no-extend-native String.prototype.replaceAll = function (target, replacement) { return this.split(target).join(replacement); }; } // Prefill admin fields try { const email = localStorage.getItem('email'); const pwd = localStorage.getItem('password'); if (email) jQuery('#shoutboxEmailAdmin').val(email); if (pwd) jQuery('#shoutboxPasswordAdmin').val(pwd); } catch (_) {} this.bindUI(); this.initSocket(); this.bootstrap(); } /* ---------- UI helpers ---------- */ parseSmileys = (text) => { Object.keys(this.smileys).forEach(s => { text = text.replaceAll(s, this.smileys[s]); }); return text; }; stripHTML = (html) => { const div = document.createElement('div'); div.innerHTML = html; return div.textContent || div.innerText || ''; }; clearChat = () => { const el = document.querySelector('#shoutChat'); if (el) el.innerHTML = ''; }; serverMessage = (text) => { const $shoutChat = jQuery('#shoutChat'); $shoutChat.append(this.sprintf(this.traductions.serverMessage, text)); $shoutChat.animate({ scrollTop: $shoutChat[0].scrollHeight }, 300); }; showAd = () => { const html = `Get your free shoutbox with no ads for 9.90โ‚ฌ/year`; this.serverMessage(html); }; getColor = (username) => { const colors = ['#FFB900','#D83B01','#B50E0E','#E81123','#B4009E','#5C2D91','#0078D7','#00B4FF','#008272','#107C10']; let sum = 0; for (let i = 0; i < username.length; i++) sum += username.charCodeAt(i); return colors[sum % colors.length]; }; getAvatar = (image, username = '') => { const color = this.getColor(username); if (image.indexOf(this.server) === 0) { const firstLetter = (username.charAt(0) || '').toUpperCase(); const secondLetter = (username.charAt(1) || '').toUpperCase(); return `
${firstLetter}${secondLetter}
`; } return this.sprintf(this.traductions.imageAvatar, image); }; receiveText = (username, message, date, scrollTimer, avatar, ip, id) => { username = this.stripHTML(username || ''); message = this.parseSmileys(message || ''); if (avatar) avatar = this.getAvatar(avatar, username); if (date) date = this.sprintf('(%s)', date); const html = this.sprintf(this.traductions.receivedText, id, ip, avatar || '', date || '', username, message); const $shoutChat = jQuery('#shoutChat'); $shoutChat.animate({ scrollTop: $shoutChat[0].scrollHeight }, scrollTimer || 0); jQuery(html).hide().appendTo('#shoutChat').fadeIn(200); if (this.myUser.isAdmin) jQuery(`div[data-id="${id}"]`).addClass('shoutboxAdmin'); }; addUser = (user) => { if (!user.username) user.username = this.getRandomUsername(); this.updateNumberUsersDisplay(); let avatar = user.avatar; if (avatar) avatar = this.getAvatar(avatar, user.username); const txt = this.sprintf(this.traductions.addUser, user.id, user.id, avatar || '', user.username); jQuery('#shoutBoxUserList').append(txt); }; updateNumberUsersDisplay = () => { const len = Object.keys(this.users || {}).length; const text = (len > 1) ? this.sprintf(this.traductions.usersOnline, len) : this.sprintf(this.traductions.userOnline, len); jQuery('#shoutBoxHeaderText').text(text); }; getRandomUsername = () => { const a = ['Small','Blue','Ugly','Big','Red','Yellow','Green','Nice','Cool']; const b = ['Bear','Dog','Banana','John','Joe','Jack','Chatter','Fish','Bird']; return `${a[Math.floor(Math.random()*a.length)]}${b[Math.floor(Math.random()*b.length)]}`; }; welcome = () => { const $in = jQuery('#shoutBoxInput'); $in.attr('placeholder', this.traductions.enterYourTextHere); this.serverMessage(this.sprintf(this.traductions.welcome, this.myUser.username)); $in.removeClass('shoutInputRed'); try { localStorage.setItem('username', this.myUser.username); } catch (_) {} }; /* ---------- Data flows ---------- */ refreshChat = async () => { this.clearChat(); const fd = new FormData(); fd.append('a', 'getLastMessages'); fd.append('id', this.myUser.room); const res = await fetch(this.AJAX, { method: 'POST', body: fd }); const messages = await res.json().catch(() => []); for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i]; this.receiveText(m.username, m.message, m.date, 0, m.avatar, m.ip, m.id); } }; getLastMessages = async () => { const fd = new FormData(); fd.append('a', 'getLastMessages'); fd.append('id', this.myUser.room); const res = await fetch(this.AJAX, { method: 'POST', body: fd }); const messages = await res.json().catch(() => []); for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i]; this.receiveText(m.username, m.message, m.date, 0, m.avatar, m.ip, m.id); } if (this.myUser.username) this.welcome(); }; /* ---------- Socket.IO ---------- */ initSocket = () => { this.shoutboxSocket = io.connect(`${this.server}:8443`); this.shoutboxSocket.on('connect', () => { const stored = this.stripHTML(localStorage.getItem('username') || ''); if (stored) this.myUser.username = stored; this.shoutboxSocket.emit('enterRoom', this.myUser); }); this.shoutboxSocket.on('roomEntered', () => { const stored = this.stripHTML(localStorage.getItem('username') || ''); if (stored) this.welcome(); }); this.shoutboxSocket.on('del', (id) => jQuery(`[data-id="${id}"]`).remove()); // FIX: correct selector (quote IP attr) this.shoutboxSocket.on('ban', (ip) => jQuery(`[data-ip="${ip}"]`).remove()); this.shoutboxSocket.on('receiveText', (user, message, ip, id) => { this.receiveText(user.username, message, '', 200, user.avatar, ip, id); try { new Audio(this.traductions.mp3).play(); } catch (_) {} }); this.shoutboxSocket.on('userChanged', (user) => { let avatar = user.avatar ? this.getAvatar(user.avatar, user.username) : ''; const txt = this.sprintf(this.traductions.addUser, user.id, user.id, avatar, user.username); jQuery(`#shoutBoxUser${user.id}`).html(txt); }); this.shoutboxSocket.on('setAdminMode', (password) => { this.setAdminMode(password); jQuery('div.shoutText').addClass('shoutboxAdmin'); }); this.shoutboxSocket.on('addUser', (user) => { this.users[user.id] = user; this.addUser(user); }); this.shoutboxSocket.on('removeUser', (user) => { delete this.users[user.id]; this.updateNumberUsersDisplay(); jQuery(`#shoutBoxUser${user.id}`).remove(); }); this.shoutboxSocket.on('error', (err) => console.log(err)); }; /* ---------- Admin & actions ---------- */ setAdminMode = (password) => { this.myUser.password = password; this.myUser.isAdmin = true; this.serverMessage(this.traductions.youAreAdminNow); jQuery('#shoutboxAdminLoginBtn').toggle(); }; sendText = () => { const $in = jQuery('#shoutBoxInput'); let text = ($in.val() || '').trim(); text = this.stripHTML(text); if (!text) return; if (!this.myUser.username) { this.myUser.username = text; $in.val(''); this.welcome(); this.shoutboxSocket.emit('changeUser', this.myUser); return; } $in.val(''); this.shoutboxSocket.emit('send', this.myUser, text); $in.prop('disabled', true); setTimeout(() => { $in.prop('disabled', false); $in.focus(); }, 800); }; /* ============ Emoji Picker ============ */ initEmojiPicker = () => { const RECENT_KEY = 'shout_recent_emojis'; const MAX_RECENT = 24; const EMOJI_LIST = [ "๐Ÿ˜€","๐Ÿ˜","๐Ÿ˜‚","๐Ÿคฃ","๐Ÿ˜ƒ","๐Ÿ˜„","๐Ÿ˜…","๐Ÿ˜†","๐Ÿ˜‰","๐Ÿ˜Š","๐Ÿ™‚","๐Ÿ™ƒ","๐Ÿ˜","๐Ÿ˜˜","๐Ÿ˜—","๐Ÿ˜™","๐Ÿ˜š","๐Ÿค—","๐Ÿคญ","๐Ÿคซ","๐Ÿค”","๐Ÿคจ","๐Ÿ˜", "๐Ÿ˜‘","๐Ÿ˜ถ","๐Ÿ™„","๐Ÿ˜","๐Ÿ˜ฃ","๐Ÿ˜ฅ","๐Ÿ˜ฎ","๐Ÿค","๐Ÿ˜ฏ","๐Ÿ˜ช","๐Ÿ˜ซ","๐Ÿฅฑ","๐Ÿ˜ด","๐Ÿ˜Œ","๐Ÿ˜›","๐Ÿ˜œ","๐Ÿคช","๐Ÿ˜","๐Ÿคค","๐Ÿ˜’","๐Ÿ˜“","๐Ÿ˜”","๐Ÿ˜•", "๐Ÿ™","โ˜น๏ธ","๐Ÿ˜–","๐Ÿ˜ž","๐Ÿ˜Ÿ","๐Ÿ˜ค","๐Ÿ˜ข","๐Ÿ˜ญ","๐Ÿ˜ฆ","๐Ÿ˜ง","๐Ÿ˜จ","๐Ÿ˜ฉ","๐Ÿคฏ","๐Ÿ˜ฌ","๐Ÿ˜ฎโ€๐Ÿ’จ","๐Ÿ˜ฑ","๐Ÿฅต","๐Ÿฅถ","๐Ÿ˜ณ","๐Ÿซ ","๐Ÿ˜‡","๐Ÿค ", "๐Ÿ˜บ","๐Ÿ˜ธ","๐Ÿ˜น","๐Ÿ˜ป","๐Ÿ˜ผ","๐Ÿ‘","๐Ÿ‘Ž","๐Ÿ‘","๐Ÿ™Œ","๐Ÿค","๐Ÿ™","๐Ÿ’ช","๐Ÿ’ฏ","๐ŸŽ‰","โœจ","๐Ÿ”ฅ","๐Ÿ’ฅ","๐ŸŒŸ","๐ŸŽˆ","๐ŸŽ","๐Ÿฅณ","โšฝ","๐Ÿ€","๐ŸŽฎ", "๐Ÿ•","๐Ÿ”","๐ŸŸ","๐ŸŒฎ","๐Ÿฃ","๐Ÿฐ","๐Ÿบ","๐Ÿท","โ˜•","๐Ÿต","๐ŸŒž","๐ŸŒ™","โญ","โ˜”","๐ŸŒˆ","๐Ÿš—","โœˆ๏ธ","๐Ÿ ","๐Ÿ“ฑ","๐Ÿ’ก", "โค๏ธ","๐Ÿงก","๐Ÿ’›","๐Ÿ’š","๐Ÿ’™","๐Ÿ’œ","๐Ÿ–ค","๐Ÿค","๐ŸคŽ" ]; const $in = jQuery('#shoutBoxInput'); if (!$in.length) return; // 1) Wrap the input if needed so we can anchor UI if (!$in.parent().hasClass('sb-inputWrap')) $in.wrap('
'); const $wrap = $in.parent(); // 2) Inject button + panel if missing if (!jQuery('#sbEmojiBtn').length) { $wrap.append(''); } if (!jQuery('#sbEmojiPanel').length) { $wrap.append( '' ); } const $btn = jQuery('#sbEmojiBtn'); const $panel = jQuery('#sbEmojiPanel'); const $recentS = jQuery('#sbEmojiRecent'); const $allS = jQuery('#sbEmojiAll'); const $search = jQuery('#sbEmojiSearch'); const $close = jQuery('#sbEmojiClose'); // Ensure input padding (so text doesn't sit under the button) const pr = parseInt(window.getComputedStyle($in.get(0)).paddingRight || '12', 10); if (pr < 54) $in.css('padding-right','54px'); // --- helpers --- const loadRecent = () => { try { return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); } catch { return []; } }; const saveRecent = (arr) => localStorage.setItem(RECENT_KEY, JSON.stringify(arr.slice(0, MAX_RECENT))); const bumpRecent = (emoji) => { const r = loadRecent().filter(e => e !== emoji); r.unshift(emoji); saveRecent(r); }; const insertAtCursor = (inputEl, text) => { const start = inputEl.selectionStart ?? inputEl.value.length; const end = inputEl.selectionEnd ?? inputEl.value.length; const val = inputEl.value; inputEl.value = val.slice(0,start) + text + val.slice(end); const pos = start + text.length; inputEl.setSelectionRange(pos, pos); inputEl.dispatchEvent(new Event('input', { bubbles: true })); inputEl.focus(); }; const buildGrid = (list, $container) => { const $grid = jQuery('
'); list.forEach(e => { jQuery(''); } if (!jQuery('#sbEmojiPanel').length) { $wrap.append( '' ); } const $btn = jQuery('#sbEmojiBtn'); const $panel = jQuery('#sbEmojiPanel'); const $recentS = jQuery('#sbEmojiRecent'); const $allS = jQuery('#sbEmojiAll'); const $search = jQuery('#sbEmojiSearch'); const $close = jQuery('#sbEmojiClose'); // Ensure input padding (so text doesn't sit under the button) const pr = parseInt(window.getComputedStyle($in.get(0)).paddingRight || '12', 10); if (pr < 54) $in.css('padding-right','54px'); // --- helpers --- const loadRecent = () => { try { return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); } catch { return []; } }; const saveRecent = (arr) => localStorage.setItem(RECENT_KEY, JSON.stringify(arr.slice(0, MAX_RECENT))); const bumpRecent = (emoji) => { const r = loadRecent().filter(e => e !== emoji); r.unshift(emoji); saveRecent(r); }; const insertAtCursor = (inputEl, text) => { const start = inputEl.selectionStart ?? inputEl.value.length; const end = inputEl.selectionEnd ?? inputEl.value.length; const val = inputEl.value; inputEl.value = val.slice(0,start) + text + val.slice(end); const pos = start + text.length; inputEl.setSelectionRange(pos, pos); inputEl.dispatchEvent(new Event('input', { bubbles: true })); inputEl.focus(); }; const buildGrid = (list, $container) => { const $grid = jQuery('
'); list.forEach(e => { jQuery('