PHP Code Editor
<?php /** * RSL Messenger V10 - Smart Focus & Power Switch * 2026 High-Performance Edition */ $db_file = __DIR__ . '/db/rsl-ws-chat-db.json'; $typing_file = __DIR__ . '/db/typing.json'; $ws_internal = "127.0.0.1:8888"; if (isset($_GET['action'])) { $raw = file_get_contents('php://input'); $data = json_decode($raw, true); if ($_GET['action'] == 'send' && $data) { $history = file_exists($db_file) ? json_decode(file_get_contents($db_file), true) : []; $data['id'] = uniqid(); $data['time'] = date('H:i'); $history[] = $data; if (count($history) > 30) array_shift($history); file_put_contents($db_file, json_encode($history)); $list = file_exists($typing_file) ? json_decode(file_get_contents($typing_file), true) : []; if(isset($data['from'])) unset($list[$data['from']]); file_put_contents($typing_file, json_encode($list)); rsl_push_ws($ws_internal, $data); exit(json_encode(['status' => 'ok'])); } if ($_GET['action'] == 'typing' && $data) { $user = $data['user']; $list = file_exists($typing_file) ? json_decode(file_get_contents($typing_file), true) : []; $list[$user] = ['target' => $data['target'], 'expire' => time() + 3]; foreach ($list as $name => $v) { if ($v['expire'] < time()) unset($list[$name]); } file_put_contents($typing_file, json_encode($list)); exit; } if ($_GET['action'] == 'poll') { $last_id = $_GET['last_id'] ?? ''; $my_name = $_GET['user'] ?? ''; $start = time(); while ((time() - $start) < 20) { clearstatcache(); $response = ['msg' => null, 'typing' => [], 'users' => ['Global']]; if (file_exists($db_file)) { $msgs = json_decode(file_get_contents($db_file), true) ?: []; foreach($msgs as $m) { if($m['from'] !== $my_name) $response['users'][] = $m['from']; } $response['users'] = array_values(array_unique($response['users'])); $last = end($msgs); if ($last && $last['id'] !== $last_id) { if ($last['target'] === 'Global' || $last['target'] === $my_name || $last['from'] === $my_name) { $response['msg'] = $last; } else { $last_id = $last['id']; } } } if (file_exists($typing_file)) { $t_list = json_decode(file_get_contents($typing_file), true) ?: []; foreach ($t_list as $name => $v) { if ($v['expire'] > time() && $name !== $my_name) $response['typing'][] = ['user' => $name, 'target' => $v['target']]; } } if ($response['msg'] || !empty($response['typing'])) { exit(json_encode($response)); } usleep(900000); } exit(json_encode(['status' => 'timeout'])); } } function rsl_push_ws($addr, $data) { $sock = @stream_socket_client("tcp://$addr", $e, $s, 1); if ($sock) { $key = base64_encode(random_bytes(16)); fwrite($sock, "GET / HTTP/1.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: $key\r\n\r\n"); fread($sock, 1024); fwrite($sock, pack('CC', 0x81, strlen($t=json_encode($data))).$t); fclose($sock); } } ?> <!DOCTYPE html> <html lang="id"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>RSL Messenger V10</title> <script src="https://cdn.tailwindcss.com"></script> <style> .chat-h { height: 100vh; } @media (min-width: 768px) { .chat-h { height: calc(100vh - 40px); } } #sidebar { transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); } .sidebar-hide { transform: translateX(-100%); } .sidebar-show { transform: translateX(0); } #box::-webkit-scrollbar { width: 4px; } .switch-on { background-color: #10b981; transform: translateX(100%); } </style> </head> <body class="bg-zinc-950 md:p-5 font-sans overflow-hidden"> <div id="login-page" class="fixed inset-0 bg-zinc-950 flex items-center justify-center z-[100]"> <div class="bg-zinc-900 p-8 rounded-3xl border border-zinc-800 shadow-2xl w-full max-w-sm text-center mx-4"> <h2 class="text-white font-black text-2xl mb-6 italic uppercase tracking-tighter">RSL LOGIN</h2> <input id="nickname" type="text" placeholder="NickName..." class="w-full bg-black border border-zinc-800 text-white p-4 rounded-xl outline-none focus:border-indigo-500 mb-4 text-center tracking-widest font-bold uppercase"> <button onclick="doLogin()" class="w-full bg-indigo-600 text-white font-bold py-4 rounded-xl active:scale-95 transition shadow-lg shadow-indigo-500/20 uppercase text-sm">Masuk</button> </div> </div> <div id="chat-page" class="max-w-6xl mx-auto flex bg-zinc-900 md:rounded-3xl shadow-2xl border-zinc-800 chat-h hidden relative overflow-hidden"> <!-- SIDEBAR --> <div id="sidebar" onmouseleave="handleSidebarLeave()" onmouseenter="handleSidebarEnter()" class="fixed md:relative inset-y-0 left-0 w-72 md:w-1/3 bg-zinc-900 border-r border-zinc-800 z-50 sidebar-hide md:translate-x-0 flex flex-col shadow-2xl"> <div class="p-4 bg-zinc-800 flex items-center justify-between border-b border-zinc-700"> <div class="flex items-center gap-2"> <div id="my-av" class="w-8 h-8 bg-indigo-600 rounded-full flex items-center justify-center text-xs font-bold text-white uppercase italic">?</div> <span id="my-name-display" class="text-xs font-bold text-white uppercase tracking-wider">User</span> </div> <button onclick="doLogout()" class="text-[9px] bg-zinc-700 text-zinc-300 px-3 py-1 rounded-md hover:bg-red-900 transition font-bold uppercase">Logout</button> </div> <div id="user-list" class="flex-1 overflow-y-auto"></div> </div> <!-- OVERLAY --> <div id="overlay" class="fixed inset-0 bg-black/70 z-40 hidden md:hidden" onclick="toggleSidebar()"></div> <!-- CONTENT --> <div class="flex-1 flex flex-col min-w-0"> <!-- HEADER --> <div class="p-4 bg-zinc-800 border-b border-zinc-700 flex justify-between items-center shadow-md"> <div class="flex items-center gap-3"> <button onclick="toggleSidebar()" class="p-2 -ml-2 text-zinc-400 hover:text-indigo-500"> <svg viewBox="0 0 24 24" class="w-6 h-6 fill-current"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg> </button> <div id="h-av" class="w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center font-bold text-white uppercase italic text-sm">G</div> <div> <h2 id="h-ti" class="text-sm font-bold text-white leading-none">Grup Global</h2> <span id="t-ui" class="text-[10px] text-indigo-400 italic font-medium"></span> </div> </div> <!-- POWER SWITCH ON/OFF --> <div class="flex items-center gap-2"> <span id="pwr-label" class="text-[9px] text-zinc-500 uppercase font-bold">Offline</span> <div onclick="togglePower()" class="w-10 h-5 bg-zinc-700 rounded-full p-1 cursor-pointer flex items-center transition-all duration-300 relative"> <div id="pwr-btn" class="w-3 h-3 bg-zinc-400 rounded-full transition-all duration-300"></div> </div> </div> </div> <div id="box" class="flex-1 p-4 md:p-6 overflow-y-auto space-y-4 bg-zinc-950/40"></div> <!-- INPUT --> <div class="p-3 md:p-4 bg-zinc-900 border-t border-zinc-800"> <div class="flex items-end gap-2"> <button onclick="toggleEmoji()" class="p-2 text-zinc-500 hover:text-indigo-500 mb-1"> <svg viewBox="0 0 24 24" class="w-6 h-6 fill-current"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z"/></svg> </button> <textarea id="m" rows="1" placeholder="Pesan..." class="flex-1 bg-black border border-zinc-800 text-white p-3 rounded-2xl outline-none focus:ring-1 focus:ring-indigo-500 resize-none max-h-32 text-sm" oninput="autoHeight(this)"></textarea> <button onclick="send()" class="bg-indigo-600 p-3 rounded-2xl text-white hover:bg-indigo-500 transition active:scale-90 mb-1"> <svg viewBox="0 0 24 24" class="w-6 h-6 fill-current"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> </button> </div> <div id="emoji-picker" class="hidden mt-2 p-3 bg-zinc-800 border border-zinc-700 rounded-xl grid grid-cols-8 gap-2"> <button onclick="addEmoji('😀')">😀</button><button onclick="addEmoji('😂')">😂</button> <button onclick="addEmoji('👍')">👍</button><button onclick="addEmoji('❤️')">❤️</button> <button onclick="addEmoji('🔥')">🔥</button><button onclick="addEmoji('😎')">😎</button> <button onclick="addEmoji('🙏')">🙏</button><button onclick="addEmoji('🎉')">🎉</button> </div> </div> </div> </div> <script> let lastId = '', target = 'Global', myMsgs = [], currentUser = null, isPolling = false, isPowerOn = false, sidebarTimer = null; // --- SIDEBAR FOCUS LOGIC --- function toggleSidebar() { const sb = document.getElementById('sidebar'), ov = document.getElementById('overlay'); sb.classList.toggle('sidebar-hide'); sb.classList.toggle('sidebar-show'); if(window.innerWidth < 768) ov.classList.toggle('hidden'); } function handleSidebarLeave() { if(window.innerWidth < 768) return; // Nonaktifkan fitur auto-hide di mobile agar tidak mengganggu sidebarTimer = setTimeout(() => { const sb = document.getElementById('sidebar'); if (sb.classList.contains('sidebar-show')) toggleSidebar(); }, 500); // Delay 500ms } function handleSidebarEnter() { if(sidebarTimer) clearTimeout(sidebarTimer); } // --- POWER LOGIC --- function togglePower() { isPowerOn = !isPowerOn; const btn = document.getElementById('pwr-btn'); const label = document.getElementById('pwr-label'); if (isPowerOn) { btn.classList.add('switch-on'); label.innerText = "Online"; label.className = "text-[9px] text-green-500 uppercase font-bold"; poll(); // Mulai polling } else { btn.classList.remove('switch-on'); label.innerText = "Offline"; label.className = "text-[9px] text-zinc-500 uppercase font-bold"; isPolling = false; // Matikan polling secara paksa } } // --- UI HELPERS --- function toggleEmoji() { document.getElementById('emoji-picker').classList.toggle('hidden'); } function addEmoji(e) { document.getElementById('m').value += e; document.getElementById('m').focus(); } function autoHeight(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; isTyping(); } document.getElementById('m').addEventListener('keydown', function(e) { if (e.key === 'Enter' && !(/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) && !e.shiftKey) { e.preventDefault(); send(); } }); // --- SESSION --- function doLogin() { const n = document.getElementById('nickname').value.trim(); if(!n) return; localStorage.setItem('rsl_session', JSON.stringify({name: n, expire: new Date().getTime() + 3600000})); currentUser = n; document.getElementById('login-page').classList.add('hidden'); document.getElementById('chat-page').classList.remove('hidden'); document.getElementById('my-name-display').innerText = currentUser; document.getElementById('my-av').innerText = currentUser.charAt(0); togglePower(); // Otomatis nyalakan saat login } function doLogout() { localStorage.removeItem('rsl_session'); currentUser = null; isPowerOn = false; location.reload(); } // --- CORE POLL (Smart Locked) --- async function poll() { if(!currentUser || !isPowerOn || isPolling) return; isPolling = true; try { const res = await fetch(`?action=poll&last_id=${lastId}&user=${encodeURIComponent(currentUser)}`); if(!res.ok) throw new Error(); const data = await res.json(); if(data.users) updateSidebar(data.users); if(data.typing) { const active = data.typing.filter(t => (target === 'Global' ? t.target === 'Global' : t.user === target && t.target === currentUser)).map(t => t.user); document.getElementById('t-ui').innerText = active.length > 0 ? active.join(", ") + " mengetik..." : ""; } else { document.getElementById('t-ui').innerText = ""; } if(data.msg) { lastId = data.msg.id; myMsgs.push(data.msg); if(data.msg.target === target || (data.msg.from === target && data.msg.target === currentUser) || (data.msg.from === currentUser && data.msg.target === target)) render(data.msg, data.msg.from === currentUser); } } catch (e) {} isPolling = false; if(isPowerOn) setTimeout(poll, 400); // Sequential Call } async function send() { if(!isPowerOn) return alert("Nyalakan Power untuk mengirim!"); const el = document.getElementById('m'), m = el.value.trim(); if(!m) return; const p = { type: target==='Global'?'group':'private', target: target, from: currentUser, message: m }; await fetch('?action=send', { method: 'POST', body: JSON.stringify(p) }); el.value = ''; el.style.height = 'auto'; } function render(d, isMe) { const box = document.getElementById('box'), div = document.createElement('div'); div.className = `flex flex-col ${isMe ? 'items-end' : 'items-start'}`; div.innerHTML = `<div class="p-3 rounded-2xl max-w-[85%] md:max-w-[75%] ${isMe ? 'bg-indigo-700 text-white rounded-tr-none' : 'bg-zinc-800 text-zinc-100 rounded-tl-none border border-zinc-700'} shadow-lg"><small class="block text-[8px] opacity-40 font-bold uppercase mb-1 tracking-widest">${d.from}</small><p class="text-sm whitespace-pre-wrap break-words">${d.message}</p></div>`; box.appendChild(div); box.scrollTop = box.scrollHeight; } function updateSidebar(users) { const list = document.getElementById('user-list'); list.innerHTML = users.map(name => `<div onclick="setChat('${name}')" class="p-4 flex items-center gap-3 cursor-pointer border-b border-zinc-800/40 transition ${target === name ? 'bg-zinc-800 border-l-4 border-indigo-500 shadow-inner' : 'hover:bg-zinc-800'}"><div class="w-10 h-10 ${name==='Global'?'bg-indigo-600':'bg-zinc-700'} rounded-full flex items-center justify-center font-bold text-white uppercase italic text-xs">${name.charAt(0)}</div><div class="flex-1"><h3 class="text-sm font-bold text-white leading-none">${name}</h3><p class="text-[10px] text-zinc-500 mt-1 uppercase">Aktif</p></div></div>`).join(''); } function setChat(t) { target = t; document.getElementById('h-ti').innerText = t; document.getElementById('h-av').innerText = t.charAt(0); document.getElementById('box').innerHTML = ''; myMsgs.forEach(m => { if((target === 'Global' && m.target === 'Global') || (m.target === currentUser && m.from === target) || (m.from === currentUser && m.target === target)) render(m, m.from === currentUser); }); if(window.innerWidth < 768) toggleSidebar(); } function isTyping() { if(currentUser && isPowerOn) fetch('?action=typing', { method: 'POST', body: JSON.stringify({ user: currentUser, target: target }) }); } // Check existing session const s = JSON.parse(localStorage.getItem('rsl_session')); if (s && new Date().getTime() < s.expire) { currentUser = s.name; doLogin(); } </script> </body> </html>
Run Code
<?php /** * RSL Messenger V10 - Smart Focus & Power Switch * 2026 High-Performance Edition */ $db_file = __DIR__ . '/db/rsl-ws-chat-db.json'; $typing_file = __DIR__ . '/db/typing.json'; $ws_internal = "127.0.0.1:8888"; if (isset($_GET['action'])) { $raw = file_get_contents('php://input'); $data = json_decode($raw, true); if ($_GET['action'] == 'send' && $data) { $history = file_exists($db_file) ? json_decode(file_get_contents($db_file), true) : []; $data['id'] = uniqid(); $data['time'] = date('H:i'); $history[] = $data; if (count($history) > 30) array_shift($history); file_put_contents($db_file, json_encode($history)); $list = file_exists($typing_file) ? json_decode(file_get_contents($typing_file), true) : []; if(isset($data['from'])) unset($list[$data['from']]); file_put_contents($typing_file, json_encode($list)); rsl_push_ws($ws_internal, $data); exit(json_encode(['status' => 'ok'])); } if ($_GET['action'] == 'typing' && $data) { $user = $data['user']; $list = file_exists($typing_file) ? json_decode(file_get_contents($typing_file), true) : []; $list[$user] = ['target' => $data['target'], 'expire' => time() + 3]; foreach ($list as $name => $v) { if ($v['expire'] < time()) unset($list[$name]); } file_put_contents($typing_file, json_encode($list)); exit; } if ($_GET['action'] == 'poll') { $last_id = $_GET['last_id'] ?? ''; $my_name = $_GET['user'] ?? ''; $start = time(); while ((time() - $start) < 20) { clearstatcache(); $response = ['msg' => null, 'typing' => [], 'users' => ['Global']]; if (file_exists($db_file)) { $msgs = json_decode(file_get_contents($db_file), true) ?: []; foreach($msgs as $m) { if($m['from'] !== $my_name) $response['users'][] = $m['from']; } $response['users'] = array_values(array_unique($response['users'])); $last = end($msgs); if ($last && $last['id'] !== $last_id) { if ($last['target'] === 'Global' || $last['target'] === $my_name || $last['from'] === $my_name) { $response['msg'] = $last; } else { $last_id = $last['id']; } } } if (file_exists($typing_file)) { $t_list = json_decode(file_get_contents($typing_file), true) ?: []; foreach ($t_list as $name => $v) { if ($v['expire'] > time() && $name !== $my_name) $response['typing'][] = ['user' => $name, 'target' => $v['target']]; } } if ($response['msg'] || !empty($response['typing'])) { exit(json_encode($response)); } usleep(900000); } exit(json_encode(['status' => 'timeout'])); } } function rsl_push_ws($addr, $data) { $sock = @stream_socket_client("tcp://$addr", $e, $s, 1); if ($sock) { $key = base64_encode(random_bytes(16)); fwrite($sock, "GET / HTTP/1.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: $key\r\n\r\n"); fread($sock, 1024); fwrite($sock, pack('CC', 0x81, strlen($t=json_encode($data))).$t); fclose($sock); } } ?> <!DOCTYPE html> <html lang="id"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>RSL Messenger V10</title> <script src="https://cdn.tailwindcss.com"></script> <style> .chat-h { height: 100vh; } @media (min-width: 768px) { .chat-h { height: calc(100vh - 40px); } } #sidebar { transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); } .sidebar-hide { transform: translateX(-100%); } .sidebar-show { transform: translateX(0); } #box::-webkit-scrollbar { width: 4px; } .switch-on { background-color: #10b981; transform: translateX(100%); } </style> </head> <body class="bg-zinc-950 md:p-5 font-sans overflow-hidden"> <div id="login-page" class="fixed inset-0 bg-zinc-950 flex items-center justify-center z-[100]"> <div class="bg-zinc-900 p-8 rounded-3xl border border-zinc-800 shadow-2xl w-full max-w-sm text-center mx-4"> <h2 class="text-white font-black text-2xl mb-6 italic uppercase tracking-tighter">RSL LOGIN</h2> <input id="nickname" type="text" placeholder="NickName..." class="w-full bg-black border border-zinc-800 text-white p-4 rounded-xl outline-none focus:border-indigo-500 mb-4 text-center tracking-widest font-bold uppercase"> <button onclick="doLogin()" class="w-full bg-indigo-600 text-white font-bold py-4 rounded-xl active:scale-95 transition shadow-lg shadow-indigo-500/20 uppercase text-sm">Masuk</button> </div> </div> <div id="chat-page" class="max-w-6xl mx-auto flex bg-zinc-900 md:rounded-3xl shadow-2xl border-zinc-800 chat-h hidden relative overflow-hidden"> <!-- SIDEBAR --> <div id="sidebar" onmouseleave="handleSidebarLeave()" onmouseenter="handleSidebarEnter()" class="fixed md:relative inset-y-0 left-0 w-72 md:w-1/3 bg-zinc-900 border-r border-zinc-800 z-50 sidebar-hide md:translate-x-0 flex flex-col shadow-2xl"> <div class="p-4 bg-zinc-800 flex items-center justify-between border-b border-zinc-700"> <div class="flex items-center gap-2"> <div id="my-av" class="w-8 h-8 bg-indigo-600 rounded-full flex items-center justify-center text-xs font-bold text-white uppercase italic">?</div> <span id="my-name-display" class="text-xs font-bold text-white uppercase tracking-wider">User</span> </div> <button onclick="doLogout()" class="text-[9px] bg-zinc-700 text-zinc-300 px-3 py-1 rounded-md hover:bg-red-900 transition font-bold uppercase">Logout</button> </div> <div id="user-list" class="flex-1 overflow-y-auto"></div> </div> <!-- OVERLAY --> <div id="overlay" class="fixed inset-0 bg-black/70 z-40 hidden md:hidden" onclick="toggleSidebar()"></div> <!-- CONTENT --> <div class="flex-1 flex flex-col min-w-0"> <!-- HEADER --> <div class="p-4 bg-zinc-800 border-b border-zinc-700 flex justify-between items-center shadow-md"> <div class="flex items-center gap-3"> <button onclick="toggleSidebar()" class="p-2 -ml-2 text-zinc-400 hover:text-indigo-500"> <svg viewBox="0 0 24 24" class="w-6 h-6 fill-current"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg> </button> <div id="h-av" class="w-10 h-10 bg-indigo-600 rounded-full flex items-center justify-center font-bold text-white uppercase italic text-sm">G</div> <div> <h2 id="h-ti" class="text-sm font-bold text-white leading-none">Grup Global</h2> <span id="t-ui" class="text-[10px] text-indigo-400 italic font-medium"></span> </div> </div> <!-- POWER SWITCH ON/OFF --> <div class="flex items-center gap-2"> <span id="pwr-label" class="text-[9px] text-zinc-500 uppercase font-bold">Offline</span> <div onclick="togglePower()" class="w-10 h-5 bg-zinc-700 rounded-full p-1 cursor-pointer flex items-center transition-all duration-300 relative"> <div id="pwr-btn" class="w-3 h-3 bg-zinc-400 rounded-full transition-all duration-300"></div> </div> </div> </div> <div id="box" class="flex-1 p-4 md:p-6 overflow-y-auto space-y-4 bg-zinc-950/40"></div> <!-- INPUT --> <div class="p-3 md:p-4 bg-zinc-900 border-t border-zinc-800"> <div class="flex items-end gap-2"> <button onclick="toggleEmoji()" class="p-2 text-zinc-500 hover:text-indigo-500 mb-1"> <svg viewBox="0 0 24 24" class="w-6 h-6 fill-current"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z"/></svg> </button> <textarea id="m" rows="1" placeholder="Pesan..." class="flex-1 bg-black border border-zinc-800 text-white p-3 rounded-2xl outline-none focus:ring-1 focus:ring-indigo-500 resize-none max-h-32 text-sm" oninput="autoHeight(this)"></textarea> <button onclick="send()" class="bg-indigo-600 p-3 rounded-2xl text-white hover:bg-indigo-500 transition active:scale-90 mb-1"> <svg viewBox="0 0 24 24" class="w-6 h-6 fill-current"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> </button> </div> <div id="emoji-picker" class="hidden mt-2 p-3 bg-zinc-800 border border-zinc-700 rounded-xl grid grid-cols-8 gap-2"> <button onclick="addEmoji('😀')">😀</button><button onclick="addEmoji('😂')">😂</button> <button onclick="addEmoji('👍')">👍</button><button onclick="addEmoji('❤️')">❤️</button> <button onclick="addEmoji('🔥')">🔥</button><button onclick="addEmoji('😎')">😎</button> <button onclick="addEmoji('🙏')">🙏</button><button onclick="addEmoji('🎉')">🎉</button> </div> </div> </div> </div> <script> let lastId = '', target = 'Global', myMsgs = [], currentUser = null, isPolling = false, isPowerOn = false, sidebarTimer = null; // --- SIDEBAR FOCUS LOGIC --- function toggleSidebar() { const sb = document.getElementById('sidebar'), ov = document.getElementById('overlay'); sb.classList.toggle('sidebar-hide'); sb.classList.toggle('sidebar-show'); if(window.innerWidth < 768) ov.classList.toggle('hidden'); } function handleSidebarLeave() { if(window.innerWidth < 768) return; // Nonaktifkan fitur auto-hide di mobile agar tidak mengganggu sidebarTimer = setTimeout(() => { const sb = document.getElementById('sidebar'); if (sb.classList.contains('sidebar-show')) toggleSidebar(); }, 500); // Delay 500ms } function handleSidebarEnter() { if(sidebarTimer) clearTimeout(sidebarTimer); } // --- POWER LOGIC --- function togglePower() { isPowerOn = !isPowerOn; const btn = document.getElementById('pwr-btn'); const label = document.getElementById('pwr-label'); if (isPowerOn) { btn.classList.add('switch-on'); label.innerText = "Online"; label.className = "text-[9px] text-green-500 uppercase font-bold"; poll(); // Mulai polling } else { btn.classList.remove('switch-on'); label.innerText = "Offline"; label.className = "text-[9px] text-zinc-500 uppercase font-bold"; isPolling = false; // Matikan polling secara paksa } } // --- UI HELPERS --- function toggleEmoji() { document.getElementById('emoji-picker').classList.toggle('hidden'); } function addEmoji(e) { document.getElementById('m').value += e; document.getElementById('m').focus(); } function autoHeight(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; isTyping(); } document.getElementById('m').addEventListener('keydown', function(e) { if (e.key === 'Enter' && !(/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) && !e.shiftKey) { e.preventDefault(); send(); } }); // --- SESSION --- function doLogin() { const n = document.getElementById('nickname').value.trim(); if(!n) return; localStorage.setItem('rsl_session', JSON.stringify({name: n, expire: new Date().getTime() + 3600000})); currentUser = n; document.getElementById('login-page').classList.add('hidden'); document.getElementById('chat-page').classList.remove('hidden'); document.getElementById('my-name-display').innerText = currentUser; document.getElementById('my-av').innerText = currentUser.charAt(0); togglePower(); // Otomatis nyalakan saat login } function doLogout() { localStorage.removeItem('rsl_session'); currentUser = null; isPowerOn = false; location.reload(); } // --- CORE POLL (Smart Locked) --- async function poll() { if(!currentUser || !isPowerOn || isPolling) return; isPolling = true; try { const res = await fetch(`?action=poll&last_id=${lastId}&user=${encodeURIComponent(currentUser)}`); if(!res.ok) throw new Error(); const data = await res.json(); if(data.users) updateSidebar(data.users); if(data.typing) { const active = data.typing.filter(t => (target === 'Global' ? t.target === 'Global' : t.user === target && t.target === currentUser)).map(t => t.user); document.getElementById('t-ui').innerText = active.length > 0 ? active.join(", ") + " mengetik..." : ""; } else { document.getElementById('t-ui').innerText = ""; } if(data.msg) { lastId = data.msg.id; myMsgs.push(data.msg); if(data.msg.target === target || (data.msg.from === target && data.msg.target === currentUser) || (data.msg.from === currentUser && data.msg.target === target)) render(data.msg, data.msg.from === currentUser); } } catch (e) {} isPolling = false; if(isPowerOn) setTimeout(poll, 400); // Sequential Call } async function send() { if(!isPowerOn) return alert("Nyalakan Power untuk mengirim!"); const el = document.getElementById('m'), m = el.value.trim(); if(!m) return; const p = { type: target==='Global'?'group':'private', target: target, from: currentUser, message: m }; await fetch('?action=send', { method: 'POST', body: JSON.stringify(p) }); el.value = ''; el.style.height = 'auto'; } function render(d, isMe) { const box = document.getElementById('box'), div = document.createElement('div'); div.className = `flex flex-col ${isMe ? 'items-end' : 'items-start'}`; div.innerHTML = `<div class="p-3 rounded-2xl max-w-[85%] md:max-w-[75%] ${isMe ? 'bg-indigo-700 text-white rounded-tr-none' : 'bg-zinc-800 text-zinc-100 rounded-tl-none border border-zinc-700'} shadow-lg"><small class="block text-[8px] opacity-40 font-bold uppercase mb-1 tracking-widest">${d.from}</small><p class="text-sm whitespace-pre-wrap break-words">${d.message}</p></div>`; box.appendChild(div); box.scrollTop = box.scrollHeight; } function updateSidebar(users) { const list = document.getElementById('user-list'); list.innerHTML = users.map(name => `<div onclick="setChat('${name}')" class="p-4 flex items-center gap-3 cursor-pointer border-b border-zinc-800/40 transition ${target === name ? 'bg-zinc-800 border-l-4 border-indigo-500 shadow-inner' : 'hover:bg-zinc-800'}"><div class="w-10 h-10 ${name==='Global'?'bg-indigo-600':'bg-zinc-700'} rounded-full flex items-center justify-center font-bold text-white uppercase italic text-xs">${name.charAt(0)}</div><div class="flex-1"><h3 class="text-sm font-bold text-white leading-none">${name}</h3><p class="text-[10px] text-zinc-500 mt-1 uppercase">Aktif</p></div></div>`).join(''); } function setChat(t) { target = t; document.getElementById('h-ti').innerText = t; document.getElementById('h-av').innerText = t.charAt(0); document.getElementById('box').innerHTML = ''; myMsgs.forEach(m => { if((target === 'Global' && m.target === 'Global') || (m.target === currentUser && m.from === target) || (m.from === currentUser && m.target === target)) render(m, m.from === currentUser); }); if(window.innerWidth < 768) toggleSidebar(); } function isTyping() { if(currentUser && isPowerOn) fetch('?action=typing', { method: 'POST', body: JSON.stringify({ user: currentUser, target: target }) }); } // Check existing session const s = JSON.parse(localStorage.getItem('rsl_session')); if (s && new Date().getTime() < s.expire) { currentUser = s.name; doLogin(); } </script> </body> </html>
Run Code New Tab
Result