commits
tags
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Livechat</title>
<script src="/static/socket.io.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f0f;
--surface: #1a1a1a;
--border: #2a2a2a;
--text: #e8e8e8;
--muted: #777;
--accent: #2563eb;
--accent-h: #1d4ed8;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 15px;
}
/* ====== AUTH SCREEN ====== */
#auth-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem 1.5rem;
gap: 1rem;
}
#auth-screen h1 {
font-size: 1.6rem;
font-weight: 700;
color: #fff;
margin-bottom: .25rem;
}
/* Tab bar */
.tab-bar {
display: flex;
width: 100%;
max-width: 340px;
background: var(--surface);
border-radius: 10px;
padding: .25rem;
gap: .25rem;
}
.tab-btn {
flex: 1;
padding: .6rem;
border: none;
border-radius: 8px;
background: transparent;
color: var(--muted);
font-size: .95rem;
font-weight: 600;
cursor: pointer;
transition: background .15s, color .15s;
}
.tab-btn.active {
background: var(--accent);
color: #fff;
}
/* Forms */
.auth-form {
display: none;
flex-direction: column;
gap: .75rem;
width: 100%;
max-width: 340px;
}
.auth-form.visible { display: flex; }
input[type=text], input[type=password] {
width: 100%;
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: .75rem 1rem;
border-radius: 10px;
font-size: 1rem;
}
input::placeholder { color: var(--muted); }
input:focus { outline: 2px solid var(--accent); border-color: transparent; }
.auth-btn {
width: 100%;
background: var(--accent);
color: #fff;
border: none;
padding: .85rem 1rem;
border-radius: 10px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
margin-top: .25rem;
}
.auth-btn:hover { background: var(--accent-h); }
.auth-btn:disabled { opacity: .5; cursor: default; }
.auth-error {
color: #f87171;
font-size: .85rem;
min-height: 1.2em;
text-align: center;
}
/* Avatar grid (register only) */
.avatar-label {
font-size: .8rem;
color: var(--muted);
font-weight: 600;
letter-spacing: .04em;
text-transform: uppercase;
}
.avatar-pick {
display: flex;
flex-direction: column;
align-items: center;
gap: .6rem;
width: 100%;
}
.avatar-pick-circle {
width: 80px; height: 80px;
border-radius: 50%;
background: var(--surface);
border: 2px dashed var(--border);
display: flex; align-items: center; justify-content: center;
overflow: hidden;
cursor: pointer;
transition: border-color .15s;
}
.avatar-pick-circle:hover { border-color: var(--accent); }
.avatar-pick-circle img { width: 100%; height: 100%; object-fit: cover; display: none; }
.avatar-pick-circle .pick-icon { font-size: 2rem; color: var(--muted); }
.avatar-pick-hint {
font-size: .8rem;
color: var(--muted);
text-align: center;
}
/* ====== CHAT SCREEN ====== */
#chat-screen {
display: none;
flex-direction: column;
height: 100vh;
}
#chat-topbar {
display: flex;
align-items: center;
gap: .6rem;
padding: .75rem 1rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#chat-topbar .me-avatar { font-size: 1.3rem; }
#chat-topbar .me-name { font-weight: 600; font-size: .95rem; }
#chat-topbar .viewers {
margin-left: auto;
font-size: .8rem;
color: var(--muted);
background: var(--bg);
padding: .2rem .6rem;
border-radius: 99px;
}
#logout-btn {
background: none;
border: none;
color: var(--muted);
font-size: 1.1rem;
cursor: pointer;
padding: .2rem .35rem;
border-radius: 6px;
}
#logout-btn:hover { color: #f87171; }
#avatar-upload-btn {
position: relative;
width: 2rem; height: 2rem;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
cursor: pointer;
border: 2px solid #333;
}
#avatar-upload-btn img {
width: 100%; height: 100%;
object-fit: cover;
}
#avatar-upload-btn .avatar-overlay {
display: none;
position: absolute;
inset: 0;
background: #0009;
font-size: .65rem;
color: #fff;
align-items: center;
justify-content: center;
}
#avatar-upload-btn:hover .avatar-overlay { display: flex; }
#avatar-file { display: none; }
#chat-log {
flex: 1;
overflow-y: auto;
padding: .75rem 1rem;
display: flex;
flex-direction: column;
gap: .55rem;
}
.cm { display: flex; gap: .5rem; align-items: flex-start; }
.cm.mine { flex-direction: row-reverse; }
.cm-avatar {
width: 1.9rem; height: 1.9rem;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1rem;
flex-shrink: 0;
overflow: hidden;
}
.cm-bubble {
max-width: 78%;
padding: .5rem .75rem;
border-radius: 14px;
font-size: .95rem;
line-height: 1.4;
word-break: break-word;
}
.cm:not(.mine) .cm-bubble {
background: var(--surface);
border-top-left-radius: 4px;
}
.cm.mine .cm-bubble {
background: var(--accent);
color: #fff;
border-top-right-radius: 4px;
}
.cm-name {
font-size: .72rem;
font-weight: 700;
margin-bottom: .15rem;
}
.cm.mine .cm-name { display: none; }
.cm.system { justify-content: center; }
.cm.system .cm-bubble {
background: transparent;
color: var(--muted);
font-size: .8rem;
font-style: italic;
text-align: center;
padding: .2rem;
}
#input-row {
display: flex;
gap: .5rem;
padding: .65rem .75rem;
background: var(--surface);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
#msg-input {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
padding: .6rem .85rem;
border-radius: 20px;
font-size: .95rem;
resize: none;
min-height: 2.4rem;
max-height: 6rem;
overflow-y: auto;
line-height: 1.4;
font-family: inherit;
}
#msg-input::placeholder { color: var(--muted); }
#msg-input:focus { outline: 2px solid var(--accent); border-color: transparent; }
#send-btn {
width: 2.4rem; height: 2.4rem;
border-radius: 50%;
background: var(--accent);
color: #fff;
border: none;
font-size: 1.1rem;
cursor: pointer;
flex-shrink: 0;
align-self: flex-end;
display: flex; align-items: center; justify-content: center;
}
#send-btn:disabled { background: #333; color: #555; cursor: default; }
#send-btn:not(:disabled):hover { background: var(--accent-h); }
/* Bifogningsknapp */
#attach-btn {
width: 2.4rem; height: 2.4rem;
border-radius: 50%;
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
font-size: 1.25rem;
cursor: pointer;
flex-shrink: 0;
align-self: flex-end;
display: flex; align-items: center; justify-content: center;
transition: background .15s;
}
#attach-btn:hover { background: #2a2a2a; }
#attach-btn.open { background: var(--accent); border-color: var(--accent); color:#fff; }
/* Bifogningspanel */
#attach-panel {
display: none;
padding: .5rem .75rem .4rem;
background: var(--surface);
border-top: 1px solid var(--border);
gap: .5rem;
flex-wrap: wrap;
}
.attach-opt {
flex: 1;
min-width: 80px;
display: flex;
flex-direction: column;
align-items: center;
gap: .3rem;
padding: .65rem .5rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 12px;
cursor: pointer;
font-size: .8rem;
color: var(--muted);
transition: border-color .15s, color .15s;
}
.attach-opt:hover { border-color: var(--accent); color: var(--text); }
.attach-opt .opt-icon { font-size: 1.6rem; line-height: 1; }
/* GIF-URL-rad */
#gif-url-row {
display: none;
gap: .4rem;
padding: .4rem .75rem .5rem;
background: var(--surface);
border-top: 1px solid var(--border);
}
#gif-url-input {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
padding: .55rem .85rem;
border-radius: 20px;
font-size: .9rem;
}
#gif-url-input::placeholder { color: var(--muted); }
#gif-url-send {
background: var(--accent); color: #fff; border: none;
padding: .55rem 1rem; border-radius: 20px; font-size: .9rem; cursor: pointer;
}
/* Upload-indikator */
#upload-indicator {
display: none;
align-items: center;
gap: .5rem;
padding: .4rem .75rem;
background: var(--surface);
border-top: 1px solid var(--border);
font-size: .82rem;
color: var(--muted);
}
#upload-bar { flex:1; height:4px; background:#333; border-radius:2px; overflow:hidden; }
#upload-fill { height:100%; background: var(--accent); width:0%; transition:width .1s; }
/* Kommandotips */
#cmd-hint {
display: none;
background: var(--surface);
border-top: 1px solid var(--border);
padding: .3rem .5rem;
flex-shrink: 0;
}
.cmd-item {
padding: .45rem .6rem;
border-radius: 8px;
font-size: .88rem;
cursor: pointer;
color: var(--text);
}
.cmd-item:hover { background: #ffffff0d; }
.cmd-item strong { color: #818cf8; }
/* Förslag-kort i chatten */
.cm-suggestion {
border-left: 3px solid var(--accent);
padding: .4rem .65rem;
background: #2563eb18;
border-radius: 0 8px 8px 0;
margin-top: .2rem;
font-size: .85rem;
}
.cm-suggestion .sug-label { color: #818cf8; font-size: .75rem; margin-bottom: .2rem; }
.cm-suggestion .sug-url { color: #e8e8e8; word-break: break-all; }
/* Inline-bilder */
.cm-img { max-width: 220px; max-height: 180px; border-radius: 8px; display: block; margin-top: .25rem; }
</style>
</head>
<body>
<!-- AUTH SCREEN -->
<div id="auth-screen">
<h1>💬 Livechat</h1>
<div class="tab-bar">
<button class="tab-btn active" id="tab-login-btn" onclick="showTab('login')">Logga in</button>
<button class="tab-btn" id="tab-register-btn" onclick="showTab('register')">Registrera</button>
</div>
<!-- Login form -->
<form class="auth-form visible" id="login-form" onsubmit="return false">
<input type="text" id="login-username" placeholder="Användarnamn" autocomplete="username" autocorrect="off" autocapitalize="none">
<input type="password" id="login-password" placeholder="Lösenord" autocomplete="current-password">
<div class="auth-error" id="login-error"></div>
<button class="auth-btn" id="login-btn" onclick="doLogin()">Logga in</button>
</form>
<!-- Register form -->
<form class="auth-form" id="register-form" onsubmit="return false">
<input type="text" id="reg-username" placeholder="Välj användarnamn" autocomplete="username" autocorrect="off" autocapitalize="none" maxlength="32">
<input type="password" id="reg-password" placeholder="Välj lösenord" autocomplete="new-password">
<div class="avatar-label">Profilbild (obligatorisk)</div>
<div class="avatar-pick">
<div class="avatar-pick-circle" id="reg-avatar-circle" onclick="document.getElementById('reg-avatar-file').click()">
<img id="reg-avatar-preview" alt="">
<span class="pick-icon" id="reg-avatar-icon">📷</span>
</div>
<div class="avatar-pick-hint">Tryck för att välja bild från kameran eller bildbiblioteket</div>
<input type="file" id="reg-avatar-file" accept="image/*" capture="environment" style="display:none">
</div>
<div class="auth-error" id="reg-error"></div>
<button class="auth-btn" id="reg-btn" onclick="doRegister()">Skapa konto</button>
</form>
</div>
<!-- CHAT SCREEN -->
<div id="chat-screen">
<div id="chat-topbar">
<div id="avatar-upload-btn" title="Byt profilbild" onclick="document.getElementById('avatar-file').click()">
<img id="me-avatar-img" src="" alt="" style="display:none">
<span id="me-avatar" style="font-size:1.3rem;display:flex;align-items:center;justify-content:center;width:100%;height:100%;"></span>
<div class="avatar-overlay">📷</div>
</div>
<input type="file" id="avatar-file" accept="image/jpeg,image/png,image/webp">
<span class="me-name" id="me-name"></span>
<span class="viewers" id="viewer-count">0 aktiva</span>
<button id="logout-btn" title="Logga ut" onclick="doLogout()">✕</button>
</div>
<div id="chat-log"></div>
<!-- Upload-indikator -->
<div id="upload-indicator">
<span id="upload-label">Laddar upp...</span>
<div id="upload-bar"><div id="upload-fill"></div></div>
</div>
<!-- GIF/bild URL-rad -->
<div id="gif-url-row">
<input id="gif-url-input" type="url" placeholder="Klistra in bild- eller GIF-URL...">
<button id="gif-url-send">Skicka</button>
</div>
<!-- Bifogningspanel -->
<div id="attach-panel" style="display:none">
<div class="attach-opt" onclick="triggerFileInput('camera')">
<span class="opt-icon">📷</span>
<span>Kamera</span>
</div>
<div class="attach-opt" onclick="triggerFileInput('gallery')">
<span class="opt-icon">🖼</span>
<span>Galleri</span>
</div>
<div class="attach-opt" onclick="showGifUrlRow()">
<span class="opt-icon">🔗</span>
<span>Bild-URL</span>
</div>
</div>
<!-- Dolda file inputs -->
<input type="file" id="file-camera" accept="image/*" capture="environment" style="display:none">
<input type="file" id="file-gallery" accept="image/*,image/gif" style="display:none">
<div id="cmd-hint">
<div class="cmd-item" onclick="insertCmd('/spela ')">🎬 <strong>/spela</strong> URL — Föreslå vad som visas på displayen</div>
</div>
<div id="input-row">
<button id="attach-btn" title="Bifoga bild">+</button>
<textarea id="msg-input" placeholder="Skriv ett meddelande..." rows="1"></textarea>
<button id="send-btn" disabled>➤</button>
</div>
</div>
<script>
const AVATARS = ['😀','😎','🤖','🦊','🐼','🐸','🦁','🐯','🦄','🐻',
'🌟','🔥','⚡','🎮','🎵','🍕','🚀','💎','🌈','👾'];
const TOKEN_KEY = 'kiosk_chat_token';
const socket = io('/chat');
let currentUser = null; // {username, emoji, color, token}
// --- Innehållsrendering ---
const _IMG_RE = /^(?:https?:\/\/|\/)[^\s"<>]+\.(gif|jpe?g|png|webp|avif)(\?[^\s"<>]*)?$/i;
function _renderContent(text) {
const t = text.trim();
if (_IMG_RE.test(t)) {
const safe = t.replace(/"/g, '%22');
return `<img src="${safe}" class="cm-img" loading="lazy" onerror="this.style.display='none'">`;
}
return escHtml(text);
}
function _suggestionHtml(url) {
const short = url.length > 55 ? url.slice(0, 52) + '…' : url;
return `<div class="cm-suggestion">
<div class="sug-label">🎬 Förslag till displayen</div>
<div class="sug-url">${escHtml(short)}</div>
</div>`;
}
function insertCmd(cmd) {
msgInput.value = cmd;
msgInput.focus();
msgInput.dispatchEvent(new Event('input'));
document.getElementById('cmd-hint').style.display = 'none';
}
// ---- Avatar pick (register) ----
let regAvatarFile = null;
document.getElementById('reg-avatar-file').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
regAvatarFile = file;
const preview = document.getElementById('reg-avatar-preview');
const icon = document.getElementById('reg-avatar-icon');
const url = URL.createObjectURL(file);
preview.src = url;
preview.style.display = 'block';
icon.style.display = 'none';
});
// ---- Tab switching ----
function showTab(tab) {
document.getElementById('login-form').classList.toggle('visible', tab === 'login');
document.getElementById('register-form').classList.toggle('visible', tab === 'register');
document.getElementById('tab-login-btn').classList.toggle('active', tab === 'login');
document.getElementById('tab-register-btn').classList.toggle('active', tab === 'register');
document.getElementById('login-error').textContent = '';
document.getElementById('reg-error').textContent = '';
// Reset avatar picker when switching tabs
regAvatarFile = null;
document.getElementById('reg-avatar-preview').style.display = 'none';
document.getElementById('reg-avatar-preview').src = '';
document.getElementById('reg-avatar-icon').style.display = '';
document.getElementById('reg-avatar-file').value = '';
}
// ---- Auth helpers ----
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
async function doLogin() {
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value;
const errEl = document.getElementById('login-error');
if (!username || !password) { errEl.textContent = 'Fyll i alla fält'; return; }
document.getElementById('login-btn').disabled = true;
try {
const r = await fetch('/api/chat/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password}),
});
const d = await r.json();
if (!r.ok) { errEl.textContent = d.error || 'Inloggning misslyckades'; return; }
localStorage.setItem(TOKEN_KEY, d.token);
enterChat(d);
} catch {
errEl.textContent = 'Nätverksfel, försök igen';
} finally {
document.getElementById('login-btn').disabled = false;
}
}
async function doRegister() {
const username = document.getElementById('reg-username').value.trim();
const password = document.getElementById('reg-password').value;
const errEl = document.getElementById('reg-error');
if (!username || !password) { errEl.textContent = 'Fyll i alla fält'; return; }
if (!regAvatarFile) { errEl.textContent = 'Välj en profilbild för att fortsätta'; return; }
document.getElementById('reg-btn').disabled = true;
try {
const randomEmoji = AVATARS[Math.floor(Math.random() * AVATARS.length)];
const r = await fetch('/api/chat/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password, emoji: randomEmoji}),
});
const d = await r.json();
if (!r.ok) { errEl.textContent = d.error || 'Registrering misslyckades'; return; }
// Upload avatar immediately after registration
const fd = new FormData();
fd.append('token', d.token);
fd.append('avatar', regAvatarFile);
const ar = await fetch('/api/chat/avatar', {method: 'POST', body: fd});
if (ar.ok) {
const ad = await ar.json();
d.avatar_url = ad.avatar_url;
}
localStorage.setItem(TOKEN_KEY, d.token);
enterChat(d);
} catch {
errEl.textContent = 'Nätverksfel, försök igen';
} finally {
document.getElementById('reg-btn').disabled = false;
}
}
function setMyAvatar(avatarUrl, emoji) {
const img = document.getElementById('me-avatar-img');
const span = document.getElementById('me-avatar');
if (avatarUrl) {
img.src = avatarUrl + '?t=' + Date.now();
img.style.display = 'block';
span.style.display = 'none';
} else {
img.style.display = 'none';
span.style.display = 'flex';
span.textContent = emoji || '?';
}
}
function enterChat(user) {
currentUser = user;
document.getElementById('auth-screen').style.display = 'none';
const cs = document.getElementById('chat-screen');
cs.style.display = 'flex';
setMyAvatar(user.avatar_url, user.emoji);
document.getElementById('me-name').textContent = user.username;
document.getElementById('msg-input').focus();
socket.emit('join', {token: user.token});
}
// ---- Avatar upload ----
document.getElementById('avatar-file').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file || !currentUser) return;
const fd = new FormData();
fd.append('token', currentUser.token);
fd.append('avatar', file);
try {
const r = await fetch('/api/chat/avatar', {method: 'POST', body: fd});
if (!r.ok) return;
const d = await r.json();
currentUser.avatar_url = d.avatar_url;
setMyAvatar(d.avatar_url, currentUser.emoji);
} catch {}
e.target.value = '';
});
function doLogout() {
localStorage.removeItem(TOKEN_KEY);
currentUser = null;
socket.disconnect();
document.getElementById('chat-screen').style.display = 'none';
document.getElementById('auth-screen').style.display = 'flex';
document.getElementById('chat-log').innerHTML = '';
document.getElementById('login-password').value = '';
socket.connect();
}
// ---- Auto-login from stored token ----
(async function tryAutoLogin() {
const token = localStorage.getItem(TOKEN_KEY);
if (!token) return;
try {
const r = await fetch('/api/chat/validate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({token}),
});
if (!r.ok) { localStorage.removeItem(TOKEN_KEY); return; }
const d = await r.json();
enterChat({...d, token});
} catch {}
})();
// ---- Socket events ----
socket.on('chat_history', (msgs) => {
if (!currentUser) return;
chatLog.innerHTML = '';
msgs.forEach(m => {
if (m.kind === 'play_suggestion') {
if (m.name === currentUser.username) _addMySuggestion(m.text);
else _addOtherSuggestion(m);
} else if (m.name === currentUser.username) {
addMyMsg(m.text);
} else {
addOtherMsg(m);
}
});
});
socket.on('chat_message', (d) => {
if (!currentUser) return;
if (d.name !== currentUser.username) addOtherMsg(d);
});
socket.on('play_suggestion', (d) => {
if (!currentUser) return;
if (d.name === currentUser.username) _addMySuggestion(d.text);
else _addOtherSuggestion(d);
});
socket.on('system_message', (d) => {
addSystemMsg(d.text);
});
socket.on('viewer_count', (d) => {
document.getElementById('viewer-count').textContent = d.count + ' aktiva';
});
socket.on('auth_error', (d) => {
localStorage.removeItem(TOKEN_KEY);
doLogout();
document.getElementById('login-error').textContent = d.error || 'Sessionen har löpt ut';
showTab('login');
});
// Re-join after reconnect
socket.on('connect', () => {
if (currentUser) socket.emit('join', {token: currentUser.token});
});
// ---- Chat input ----
const chatLog = document.getElementById('chat-log');
const msgInput = document.getElementById('msg-input');
const sendBtn = document.getElementById('send-btn');
const cmdHint = document.getElementById('cmd-hint');
msgInput.addEventListener('input', () => {
const val = msgInput.value;
sendBtn.disabled = val.trim().length === 0;
msgInput.style.height = '';
msgInput.style.height = Math.min(msgInput.scrollHeight, 96) + 'px';
cmdHint.style.display = val.startsWith('/') ? 'block' : 'none';
});
msgInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!sendBtn.disabled) sendBtn.click();
}
if (e.key === 'Escape') cmdHint.style.display = 'none';
});
sendBtn.addEventListener('click', () => {
const text = msgInput.value.trim();
if (!text || !currentUser) return;
const playMatch = text.match(/^\/spela\s+(\S+)/i);
if (playMatch) {
const url = playMatch[1];
if (!/^https?:\/\//i.test(url)) {
addSystemMsg('Ange en giltig URL: /spela https://...');
} else {
socket.emit('play_suggestion', { url });
}
} else {
socket.emit('chat_message', { text });
addMyMsg(text);
}
msgInput.value = '';
msgInput.style.height = '';
sendBtn.disabled = true;
cmdHint.style.display = 'none';
});
// ---- Message renderers ----
function myAvatarHtml() {
if (currentUser.avatar_url) return `<img src="${currentUser.avatar_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
return escHtml(currentUser.emoji);
}
function addMyMsg(text) {
const div = document.createElement('div');
div.className = 'cm mine';
div.innerHTML = `
<div class="cm-avatar" style="background:#2563eb22;color:#60a5fa">${myAvatarHtml()}</div>
<div class="cm-bubble">${_renderContent(text)}</div>`;
chatLog.appendChild(div);
chatLog.scrollTop = chatLog.scrollHeight;
}
function otherAvatarHtml(d) {
if (d.avatar_url) return `<img src="${d.avatar_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
return escHtml(d.emoji || d.name.charAt(0));
}
function addOtherMsg(d) {
const div = document.createElement('div');
div.className = 'cm';
div.innerHTML = `
<div class="cm-avatar" style="background:${d.color}22;color:${d.color}">${otherAvatarHtml(d)}</div>
<div>
<div class="cm-name" style="color:${d.color}">${escHtml(d.name)}</div>
<div class="cm-bubble">${_renderContent(d.text)}</div>
</div>`;
chatLog.appendChild(div);
chatLog.scrollTop = chatLog.scrollHeight;
}
function _addMySuggestion(url) {
const div = document.createElement('div');
div.className = 'cm mine';
div.innerHTML = `
<div class="cm-avatar" style="background:#2563eb22;color:#60a5fa">${myAvatarHtml()}</div>
<div>${_suggestionHtml(url)}</div>`;
chatLog.appendChild(div);
chatLog.scrollTop = chatLog.scrollHeight;
}
function _addOtherSuggestion(d) {
const div = document.createElement('div');
div.className = 'cm';
div.innerHTML = `
<div class="cm-avatar" style="background:${d.color}22;color:${d.color}">${otherAvatarHtml(d)}</div>
<div>
<div class="cm-name" style="color:${d.color}">${escHtml(d.name)}</div>
${_suggestionHtml(d.text)}
</div>`;
chatLog.appendChild(div);
chatLog.scrollTop = chatLog.scrollHeight;
}
function addSystemMsg(text) {
const div = document.createElement('div');
div.className = 'cm system';
div.innerHTML = `<div class="cm-bubble">${escHtml(text)}</div>`;
chatLog.appendChild(div);
chatLog.scrollTop = chatLog.scrollHeight;
}
window.addEventListener('beforeunload', () => socket.disconnect());
// ---- Bifogning ----
const attachBtn = document.getElementById('attach-btn');
const attachPanel = document.getElementById('attach-panel');
const gifUrlRow = document.getElementById('gif-url-row');
const gifUrlInput = document.getElementById('gif-url-input');
attachBtn.addEventListener('click', () => {
const open = attachPanel.style.display !== 'none';
attachPanel.style.display = open ? 'none' : 'flex';
gifUrlRow.style.display = 'none';
attachBtn.classList.toggle('open', !open);
});
function closeAttachPanel() {
attachPanel.style.display = 'none';
gifUrlRow.style.display = 'none';
attachBtn.classList.remove('open');
}
function triggerFileInput(mode) {
closeAttachPanel();
document.getElementById(mode === 'camera' ? 'file-camera' : 'file-gallery').click();
}
function showGifUrlRow() {
attachPanel.style.display = 'none';
gifUrlRow.style.display = 'flex';
gifUrlInput.value = '';
gifUrlInput.focus();
}
document.getElementById('gif-url-send').addEventListener('click', () => {
const url = gifUrlInput.value.trim();
if (!url || !/^https?:\/\//i.test(url)) return;
socket.emit('chat_message', { text: url });
addMyMsg(url);
gifUrlInput.value = '';
gifUrlRow.style.display = 'none';
msgInput.focus();
});
gifUrlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') document.getElementById('gif-url-send').click();
if (e.key === 'Escape') { gifUrlRow.style.display = 'none'; msgInput.focus(); }
});
async function uploadAndSendImage(file) {
if (!currentUser) return;
const indicator = document.getElementById('upload-indicator');
const fill = document.getElementById('upload-fill');
const label = document.getElementById('upload-label');
indicator.style.display = 'flex';
fill.style.width = '0%';
label.textContent = 'Laddar upp...';
const fd = new FormData();
fd.append('token', currentUser.token);
fd.append('image', file);
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) fill.style.width = Math.round(e.loaded / e.total * 100) + '%';
};
xhr.onload = () => {
indicator.style.display = 'none';
if (xhr.status === 200) {
const d = JSON.parse(xhr.responseText);
socket.emit('chat_message', { text: d.url });
addMyMsg(d.url);
} else {
addSystemMsg('Uppladdning misslyckades');
}
};
xhr.onerror = () => {
indicator.style.display = 'none';
addSystemMsg('Nätverksfel vid uppladdning');
};
xhr.open('POST', '/api/chat/image');
xhr.send(fd);
}
document.getElementById('file-camera').addEventListener('change', (e) => {
if (e.target.files[0]) uploadAndSendImage(e.target.files[0]);
e.target.value = '';
});
document.getElementById('file-gallery').addEventListener('change', (e) => {
if (e.target.files[0]) uploadAndSendImage(e.target.files[0]);
e.target.value = '';
});
// Stäng panel vid klick utanför
document.addEventListener('click', (e) => {
if (!attachBtn.contains(e.target) && !attachPanel.contains(e.target) && !gifUrlRow.contains(e.target)) {
closeAttachPanel();
}
});
</script>
</body>
</html>