Spaces:
Sleeping
Sleeping
Download static/script.js from smodusermc/InfinityChat: direct link, hf CLI and curl.
- Browser
- Download file 19.1 kB
-
https://huggingface.co/spaces/smodusermc/InfinityChat/resolve/7aff134a4d969d0dcd921271906a4b4be5bdfd0d/static/script.js
- Command line
-
hf download hf://spaces/smodusermc/InfinityChat@7aff134a4d969d0dcd921271906a4b4be5bdfd0d/static/script.js
-
curl -L -o script.js https://huggingface.co/spaces/smodusermc/InfinityChat/resolve/7aff134a4d969d0dcd921271906a4b4be5bdfd0d/static/script.js
19.1 kB
| (function() { | |
| 'use strict'; | |
| // --- State --- | |
| let currentUser = null; | |
| let token = localStorage.getItem('token'); | |
| let ws = null; | |
| let messages = {}; // id -> msg object | |
| let messageIds = []; // ordered | |
| let oldestMessageId = null; | |
| let replyToId = null; | |
| let typingTimer = null; | |
| let pendingFile = null; // {file_path, file_type, file_name} | |
| const API = window.location.origin; | |
| // DOM elements | |
| const authScreen = document.getElementById('auth-screen'); | |
| const chatScreen = document.getElementById('chat-screen'); | |
| const authForm = document.getElementById('auth-form'); | |
| const authUsername = document.getElementById('auth-username'); | |
| const authPassword = document.getElementById('auth-password'); | |
| const authDisplayName = document.getElementById('auth-display-name'); | |
| const displayNameGroup = document.getElementById('display-name-group'); | |
| const authSubmit = document.getElementById('auth-submit'); | |
| const authError = document.getElementById('auth-error'); | |
| const loginTab = document.getElementById('login-tab'); | |
| const signupTab = document.getElementById('signup-tab'); | |
| const messagesList = document.getElementById('messages-list'); | |
| const composeForm = document.getElementById('compose-form'); | |
| const messageInput = document.getElementById('message-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const attachBtn = document.getElementById('attach-btn'); | |
| const fileInput = document.getElementById('file-input'); | |
| const replyPreview = document.getElementById('reply-preview'); | |
| const replyPreviewText = document.getElementById('reply-preview-text'); | |
| const cancelReply = document.getElementById('cancel-reply'); | |
| const typingIndicator = document.getElementById('typing-indicator'); | |
| const onlineCountSpan = document.getElementById('online-count'); | |
| const loadOlderBtn = document.getElementById('load-older-btn'); | |
| const settingsBtn = document.getElementById('settings-btn'); | |
| const logoutBtn = document.getElementById('logout-btn'); | |
| const settingsModal = document.getElementById('settings-modal'); | |
| const saveSettingsBtn = document.getElementById('save-settings'); | |
| const closeSettingsBtn = document.getElementById('close-settings'); | |
| const themeSelect = document.getElementById('theme-select'); | |
| const customThemeSettings = document.getElementById('custom-theme-settings'); | |
| const customSelfColor = document.getElementById('custom-self-color'); | |
| const customOtherColor = document.getElementById('custom-other-color'); | |
| const customTextColor = document.getElementById('custom-text-color'); | |
| const settingDisplayName = document.getElementById('setting-display-name'); | |
| const settingAvatar = document.getElementById('setting-avatar'); | |
| const avatarPreview = document.getElementById('avatar-preview'); | |
| const uploadProgress = document.getElementById('upload-progress'); | |
| const uploadBar = document.getElementById('upload-bar'); | |
| const uploadFilename = document.getElementById('upload-filename'); | |
| let isLogin = true; | |
| // --- Theme --- | |
| function applyTheme(name) { | |
| document.body.className = name; | |
| localStorage.setItem('theme', name); | |
| if (name === 'theme-custom') { | |
| const self = localStorage.getItem('customSelfColor') || '#25D366'; | |
| const other = localStorage.getItem('customOtherColor') || '#E5E5EA'; | |
| const txt = localStorage.getItem('customTextColor') || '#000'; | |
| document.documentElement.style.setProperty('--bubble-self', self); | |
| document.documentElement.style.setProperty('--bubble-other', other); | |
| document.documentElement.style.setProperty('--bubble-text-self', txt); | |
| document.documentElement.style.setProperty('--bubble-text-other', txt); | |
| } | |
| } | |
| function loadTheme() { | |
| const saved = localStorage.getItem('theme') || 'theme-light'; | |
| themeSelect.value = saved; | |
| if (saved === 'theme-custom') customThemeSettings.classList.remove('hidden'); | |
| applyTheme(saved); | |
| } | |
| // --- Auth --- | |
| loginTab.addEventListener('click', ()=>{ | |
| isLogin=true; loginTab.classList.add('active'); signupTab.classList.remove('active'); | |
| displayNameGroup.style.display='none'; authSubmit.textContent='Login'; | |
| }); | |
| signupTab.addEventListener('click', ()=>{ | |
| isLogin=false; signupTab.classList.add('active'); loginTab.classList.remove('active'); | |
| displayNameGroup.style.display='block'; authSubmit.textContent='Sign Up'; | |
| }); | |
| authForm.addEventListener('submit', async (e)=>{ | |
| e.preventDefault(); | |
| const u = authUsername.value.trim(), p = authPassword.value, d = authDisplayName.value.trim(); | |
| if(!u||!p){ authError.textContent='Fill all fields.'; return; } | |
| try{ | |
| const url = isLogin ? `${API}/login?username=${encodeURIComponent(u)}&password=${encodeURIComponent(p)}` | |
| : `${API}/signup?username=${encodeURIComponent(u)}&password=${encodeURIComponent(p)}&display_name=${encodeURIComponent(d)}`; | |
| const res = await fetch(url, {method:'POST'}); | |
| if(!res.ok) throw new Error((await res.json()).detail||'Auth failed'); | |
| const data = await res.json(); | |
| token = data.token; | |
| currentUser = { id: data.id, username: data.username, display_name: data.display_name, avatar_path: data.avatar_path }; | |
| localStorage.setItem('token', token); | |
| showChat(); | |
| }catch(err){ authError.textContent=err.message; } | |
| }); | |
| async function autoLogin() { | |
| if(!token) return false; | |
| try{ | |
| const res = await fetch(`${API}/me`, {headers:{token}}); | |
| if(!res.ok){ token=null; localStorage.removeItem('token'); return false; } | |
| currentUser = await res.json(); | |
| return true; | |
| }catch{ return false; } | |
| } | |
| function showChat() { | |
| authScreen.classList.add('hidden'); | |
| chatScreen.classList.remove('hidden'); | |
| connectWS(); | |
| loadMessages(); | |
| } | |
| // --- WebSocket --- | |
| function connectWS() { | |
| if(ws && ws.readyState===WebSocket.OPEN) return; | |
| const proto = location.protocol==='https:'?'wss:':'ws:'; | |
| ws = new WebSocket(`${proto}//${location.host}/ws?token=${encodeURIComponent(token)}`); | |
| ws.onopen = ()=>{ console.log('WS open'); }; | |
| ws.onmessage = (ev)=> handleWSMessage(JSON.parse(ev.data)); | |
| ws.onclose = ()=> setTimeout(connectWS, 3000); | |
| ws.onerror = ()=> {}; | |
| } | |
| function handleWSMessage(msg) { | |
| switch(msg.type){ | |
| case 'online_users': onlineCountSpan.textContent = `(${msg.users.length} online)`; break; | |
| case 'user_online': break; | |
| case 'user_offline': break; | |
| case 'new_message': | |
| if(messages[msg.id]) return; | |
| messages[msg.id] = msg; | |
| messageIds.push(msg.id); | |
| renderMessage(msg); | |
| scrollBottom(); | |
| if(document.visibilityState==='visible') ws.send(JSON.stringify({type:'mark_read', up_to_message_id:msg.id})); | |
| break; | |
| case 'messages_batch': | |
| if(msg.messages.length>0){ | |
| for(const m of msg.messages){ | |
| if(!messages[m.id]){ messages[m.id]=m; messageIds.unshift(m.id); } | |
| } | |
| renderAll(); | |
| scrollBottom(); | |
| } | |
| if(msg.messages.length<50) loadOlderBtn.classList.add('hidden'); | |
| else loadOlderBtn.classList.remove('hidden'); | |
| break; | |
| case 'typing_update': | |
| typingIndicator.textContent = msg.is_typing ? `${msg.username} is typing…` : ''; break; | |
| case 'edit_message': | |
| if(messages[msg.message_id]){ | |
| messages[msg.message_id].content = msg.content; | |
| messages[msg.message_id].is_edited = true; | |
| updateBubble(msg.message_id); | |
| } | |
| break; | |
| case 'delete_message': | |
| if(messages[msg.message_id]){ | |
| messages[msg.message_id].is_deleted = true; | |
| updateBubble(msg.message_id); | |
| } | |
| break; | |
| case 'read_receipt': | |
| if(messages[msg.message_id] && messages[msg.message_id].sender_id === currentUser.id){ | |
| // Update status to "read" | |
| messages[msg.message_id].status = 'read'; | |
| updateBubble(msg.message_id); | |
| } | |
| break; | |
| } | |
| } | |
| // --- Render --- | |
| function renderMessage(msg) { | |
| const el = document.getElementById(`msg-${msg.id}`); | |
| if(el){ | |
| updateBubble(msg.id); | |
| return; | |
| } | |
| const div = document.createElement('div'); | |
| div.id = `msg-${msg.id}`; | |
| div.className = `message-bubble ${msg.sender_id===currentUser.id?'message-self':'message-other'}`; | |
| div.innerHTML = buildHTML(msg); | |
| messagesList.appendChild(div); | |
| } | |
| function buildHTML(msg) { | |
| if(msg.is_deleted) return `<em>This message was deleted</em>`; | |
| let html = ''; | |
| if(msg.reply_to_id && messages[msg.reply_to_id]){ | |
| const reply = messages[msg.reply_to_id]; | |
| html += `<div class="reply-indicator">↩ ${reply.display_name||reply.username}: ${truncate(reply.content,40)}</div>`; | |
| } | |
| html += `<div class="message-info"><span>${msg.display_name||msg.username}</span><span class="timestamp">${formatTime(msg.timestamp_ms)}</span></div>`; | |
| html += `<div class="message-text">${escapeHtml(msg.content)}</div>`; | |
| if(msg.is_edited) html += ` <em class="edited-label">[edited]</em>`; | |
| if(msg.file_path) html += renderAttachment(msg); | |
| if(msg.sender_id===currentUser.id && !msg.is_deleted){ | |
| const statusIcon = msg.status==='read' ? '✓✓' : '✓'; | |
| html += `<div class="read-status double-check">${statusIcon}</div>`; | |
| html += `<div class="message-actions"> | |
| <button onclick="window.editMsg(${msg.id})">✏️</button> | |
| <button onclick="window.deleteMsg(${msg.id})">🗑️</button> | |
| <button onclick="window.replyTo(${msg.id})">↩</button> | |
| </div>`; | |
| } | |
| return html; | |
| } | |
| function renderAttachment(msg) { | |
| const url = `${API}/download/${msg.file_path}?token=${encodeURIComponent(token)}`; | |
| if(msg.file_type.startsWith('image/')) return `<div class="attachment"><img src="${url}" alt="${msg.file_name}" onerror="this.parentElement.innerHTML='Failed to decode'"></div>`; | |
| if(msg.file_type.startsWith('video/')) return `<div class="attachment"><video controls><source src="${url}" type="${msg.file_type}"></video></div>`; | |
| if(msg.file_type.startsWith('audio/')) return `<div class="attachment"><audio controls src="${url}"></audio></div>`; | |
| return `<div class="attachment"><div class="file-preview" onclick="window.open('${url}')">📄 ${escapeHtml(msg.file_name)}</div></div>`; | |
| } | |
| function updateBubble(id) { | |
| const el = document.getElementById(`msg-${id}`); | |
| if(!el) return; | |
| const msg = messages[id]; | |
| el.className = `message-bubble ${msg.sender_id===currentUser.id?'message-self':'message-other'}`; | |
| if(msg.is_deleted) el.classList.add('deleted-bubble'); | |
| el.innerHTML = buildHTML(msg); | |
| } | |
| function renderAll() { | |
| messagesList.innerHTML = ''; | |
| messageIds.forEach(id => { if(messages[id]) { renderMessage(messages[id]); } }); | |
| } | |
| function scrollBottom() { | |
| document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight; | |
| } | |
| // --- Actions --- | |
| window.replyTo = function(id){ replyToId=id; const m=messages[id]; if(m){ replyPreviewText.textContent=`Replying to ${m.display_name||m.username}: ${truncate(m.content,30)}`; replyPreview.classList.remove('hidden'); } }; | |
| cancelReply.addEventListener('click', ()=>{ replyToId=null; replyPreview.classList.add('hidden'); }); | |
| window.editMsg = function(id){ | |
| const newContent = prompt('Edit:', messages[id]?.content); | |
| if(newContent!==null && newContent.trim()!=='') ws.send(JSON.stringify({type:'edit_message', message_id:id, content:newContent})); | |
| }; | |
| window.deleteMsg = function(id){ | |
| if(confirm('Delete permanently?')) ws.send(JSON.stringify({type:'delete_message', message_id:id})); | |
| }; | |
| // --- Sending --- | |
| composeForm.addEventListener('submit', (e)=>{ | |
| e.preventDefault(); | |
| const content = messageInput.value.trim(); | |
| if(!content && !pendingFile) return; | |
| const msg = { type:'chat_message', content, reply_to_id:replyToId, client_id:Date.now().toString(36)+Math.random().toString(36).slice(2) }; | |
| if(pendingFile){ | |
| msg.file_path = pendingFile.file_path; | |
| msg.file_type = pendingFile.file_type; | |
| msg.file_name = pendingFile.file_name; | |
| pendingFile = null; | |
| } | |
| ws.send(JSON.stringify(msg)); | |
| messageInput.value = ''; | |
| clearReply(); | |
| }); | |
| function clearReply(){ replyToId=null; replyPreview.classList.add('hidden'); } | |
| // -- Typing -- | |
| messageInput.addEventListener('input', ()=>{ | |
| ws.send(JSON.stringify({type:'typing', is_typing:true})); | |
| clearTimeout(typingTimer); | |
| typingTimer = setTimeout(()=> ws.send(JSON.stringify({type:'typing', is_typing:false})), 2000); | |
| }); | |
| // -- File upload (chunked) -- | |
| attachBtn.addEventListener('click', ()=> fileInput.click()); | |
| fileInput.addEventListener('change', async (e)=>{ | |
| const file = e.target.files[0]; | |
| if(!file) return; | |
| const CHUNK = 5*1024*1024; | |
| const total = Math.ceil(file.size / CHUNK); | |
| const uploadId = Date.now().toString(36)+Math.random().toString(36).slice(2); | |
| uploadProgress.classList.remove('hidden'); | |
| uploadFilename.textContent = file.name; | |
| uploadBar.value=0; uploadBar.max=100; | |
| for(let i=0;i<total;i++){ | |
| const start = i*CHUNK, end = Math.min(start+CHUNK, file.size); | |
| const blob = file.slice(start,end); | |
| const fd = new FormData(); fd.append('file', blob); | |
| const url = `${API}/upload_chunk?chunk_index=${i}&total_chunks=${total}&file_name=${encodeURIComponent(file.name)}&file_type=${encodeURIComponent(file.type||'application/octet-stream')}&upload_id=${uploadId}`; | |
| try{ | |
| const res = await fetch(url, {method:'POST', body:fd, headers:{token}}); | |
| if(!res.ok) throw new Error('Upload failed'); | |
| const data = await res.json(); | |
| if(data.status==='complete'){ | |
| pendingFile = {file_path:data.file_path, file_type:data.file_type, file_name:data.file_name}; | |
| alert('File ready! Add a message and send.'); | |
| uploadProgress.classList.add('hidden'); | |
| break; | |
| } | |
| uploadBar.value = ((i+1)/total)*100; | |
| }catch(err){ | |
| alert('Upload error: '+err.message); | |
| uploadProgress.classList.add('hidden'); | |
| break; | |
| } | |
| } | |
| fileInput.value=''; | |
| }); | |
| // -- Load older -- | |
| loadOlderBtn.addEventListener('click', ()=>{ | |
| if(ws) ws.send(JSON.stringify({type:'load_messages', before_id: oldestMessageId, limit:50})); | |
| }); | |
| function loadMessages(){ | |
| if(ws && ws.readyState===WebSocket.OPEN) ws.send(JSON.stringify({type:'load_messages', limit:50})); | |
| } | |
| // --- Settings --- | |
| settingsBtn.addEventListener('click', ()=>{ | |
| settingDisplayName.value = currentUser?.display_name || ''; | |
| if(currentUser?.avatar_path){ | |
| avatarPreview.src = `${API}/download/${currentUser.avatar_path}?token=${token}`; | |
| avatarPreview.style.display='block'; | |
| }else avatarPreview.style.display='none'; | |
| settingsModal.classList.remove('hidden'); | |
| }); | |
| closeSettingsBtn.addEventListener('click', ()=> settingsModal.classList.add('hidden')); | |
| saveSettingsBtn.addEventListener('click', async ()=>{ | |
| const newName = settingDisplayName.value.trim(); | |
| if(newName !== currentUser.display_name){ | |
| await fetch(`${API}/update_profile?display_name=${encodeURIComponent(newName)}`, {method:'POST', headers:{token}}); | |
| currentUser.display_name = newName; | |
| } | |
| const avatarFile = settingAvatar.files[0]; | |
| if(avatarFile){ | |
| const fd = new FormData(); fd.append('file', avatarFile); | |
| const res = await fetch(`${API}/upload_avatar`, {method:'POST', body:fd, headers:{token}}); | |
| if(res.ok){ | |
| const data = await res.json(); | |
| currentUser.avatar_path = data.avatar_path; | |
| avatarPreview.src = `${API}/download/${data.avatar_path}?token=${token}`; | |
| } | |
| } | |
| const theme = themeSelect.value; | |
| if(theme==='theme-custom'){ | |
| localStorage.setItem('customSelfColor', customSelfColor.value); | |
| localStorage.setItem('customOtherColor', customOtherColor.value); | |
| localStorage.setItem('customTextColor', customTextColor.value); | |
| } | |
| applyTheme(theme); | |
| settingsModal.classList.add('hidden'); | |
| }); | |
| themeSelect.addEventListener('change', (e)=>{ | |
| if(e.target.value==='theme-custom') customThemeSettings.classList.remove('hidden'); | |
| else customThemeSettings.classList.add('hidden'); | |
| }); | |
| logoutBtn.addEventListener('click', async ()=>{ | |
| if(ws) ws.close(); | |
| if(token) await fetch(`${API}/logout?token=${encodeURIComponent(token)}`, {method:'GET'}); | |
| localStorage.removeItem('token'); | |
| token=null; currentUser=null; messages={}; messageIds=[]; | |
| authScreen.classList.remove('hidden'); chatScreen.classList.add('hidden'); | |
| }); | |
| // --- Notification permission --- | |
| if(window.Notification && Notification.permission==='default') Notification.requestPermission(); | |
| // --- Utils --- | |
| function truncate(s,n){ return s.length>n ? s.slice(0,n-1)+'…' : s; } | |
| function escapeHtml(t){ const d=document.createElement('div'); d.textContent=t; return d.innerHTML; } | |
| function formatTime(ms){ | |
| const d = new Date(ms), now = new Date(); | |
| if(d.toDateString()===now.toDateString()) return `Today at ${d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}`; | |
| return d.toLocaleDateString() +' '+ d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}); | |
| } | |
| // -- Init -- | |
| loadTheme(); | |
| (async ()=>{ | |
| if(token && await autoLogin()) showChat(); | |
| })(); | |
| })(); |