Merge pull request #2 from metacube2/codex/lese-chat-programm-und-warte-auf-instruktionen-yb6c69

Enable admin login and dashboard
This commit is contained in:
2025-11-02 20:23:11 +01:00
committed by GitHub
+678 -45
View File
@@ -375,6 +375,7 @@ function cleanupOldData() {
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
session_start(); session_start();
$isAdminPage = isset($_GET['admin']);
function isLoggedIn() { function isLoggedIn() {
return isset($_SESSION['user_id']) && isset($_SESSION['username']); return isset($_SESSION['user_id']) && isset($_SESSION['username']);
@@ -1155,7 +1156,32 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
]); ]);
exit; exit;
} }
if ($action === 'admin_get_banned_users') {
$db = getDB();
$result = $db->query('
SELECT id, username, user_id as display_id, ban_reason, last_seen
FROM users
WHERE is_banned = 1
ORDER BY last_seen DESC
LIMIT 100
');
$banned = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$banned[] = [
'id' => $row['id'],
'display_name' => $row['username'] . '#' . $row['display_id'],
'reason' => $row['ban_reason'] ?? 'keine Angabe',
'last_seen' => $row['last_seen']
];
}
echo json_encode(['success' => true, 'banned' => $banned]);
exit;
}
// ─────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────
// ADMIN: BAN USER // ADMIN: BAN USER
// ─────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────
@@ -1493,7 +1519,254 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
/* ═══════════════════════════════════════════════════════════ */
/* ADMIN VIEWS */
/* ═══════════════════════════════════════════════════════════ */
.admin-login-container {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
width: 100%;
max-width: 450px;
}
.admin-login-container h1 {
text-align: center;
font-size: 28px;
margin-bottom: 10px;
color: #4c51bf;
}
.admin-login-container p {
text-align: center;
color: #666;
margin-bottom: 25px;
}
.admin-login-container .form-group {
margin-bottom: 20px;
}
.admin-login-container label {
display: block;
margin-bottom: 6px;
font-weight: 600;
color: #333;
}
.admin-login-container input {
width: 100%;
padding: 12px 14px;
border-radius: 10px;
border: 2px solid #e0e0e0;
font-size: 15px;
transition: border-color 0.2s ease;
}
.admin-login-container input:focus {
outline: none;
border-color: #667eea;
}
.admin-login-container button {
width: 100%;
padding: 14px;
border: none;
border-radius: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s ease;
}
.admin-login-container button:hover {
transform: translateY(-2px);
}
.admin-login-container .back-link {
margin-top: 20px;
text-align: center;
}
.admin-login-container .back-link a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.admin-login-container .back-link a:hover {
text-decoration: underline;
}
.admin-dashboard {
width: 95%;
max-width: 1400px;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 30px;
display: flex;
flex-direction: column;
gap: 30px;
}
.admin-dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.admin-dashboard-header h1 {
font-size: 26px;
color: #4c51bf;
}
.admin-dashboard-header button {
padding: 10px 18px;
border: none;
border-radius: 8px;
background: #ef4444;
color: white;
font-weight: 600;
cursor: pointer;
}
.admin-dashboard-header button:hover {
background: #dc2626;
}
.admin-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 20px;
}
.admin-stat-card {
padding: 20px;
border-radius: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
flex-direction: column;
gap: 6px;
box-shadow: 0 12px 30px rgba(102, 126, 234, 0.35);
}
.admin-stat-card span {
font-size: 13px;
opacity: 0.85;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.admin-stat-card strong {
font-size: 28px;
}
.admin-sections {
display: grid;
gap: 30px;
}
.admin-section {
background: #f9fafb;
border-radius: 16px;
padding: 20px;
border: 1px solid #e5e7eb;
}
.admin-section h2 {
font-size: 18px;
margin-bottom: 15px;
color: #1f2937;
}
.admin-table-wrapper {
overflow-x: auto;
}
.admin-table {
width: 100%;
border-collapse: collapse;
}
.admin-table th,
.admin-table td {
text-align: left;
padding: 12px 10px;
border-bottom: 1px solid #e5e7eb;
vertical-align: top;
}
.admin-table th {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
}
.admin-action-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.admin-action-buttons button {
border: none;
border-radius: 6px;
padding: 6px 10px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-secondary {
background: #e5e7eb;
color: #111827;
}
.btn-secondary:hover {
background: #d1d5db;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.admin-empty-state {
text-align: center;
padding: 20px;
color: #6b7280;
font-size: 14px;
}
.admin-error-message {
margin-bottom: 15px;
color: #dc2626;
text-align: center;
display: none;
}
.error-message { .error-message {
background: #fee; background: #fee;
color: #c33; color: #c33;
@@ -1847,9 +2120,72 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
</head> </head>
<body> <body>
<?php if (!isLoggedIn()): ?> <?php if (isAdmin()): ?>
<div class="admin-dashboard">
<div class="admin-dashboard-header">
<h1>🔐 Admin-Dashboard</h1>
<button id="adminLogoutBtn">Logout</button>
</div>
<div class="admin-stats-grid" id="adminStatsGrid">
<!-- Stats injected via JS -->
</div>
<div class="admin-sections">
<div class="admin-section">
<div class="admin-section-header">
<h2>🚨 Offene Meldungen</h2>
</div>
<div id="adminReportsContainer" class="admin-table-wrapper">
<div class="admin-empty-state">Lade Meldungen…</div>
</div>
</div>
<div class="admin-section">
<h2>🚩 Markierte Nachrichten</h2>
<div id="adminFlaggedContainer" class="admin-table-wrapper">
<div class="admin-empty-state">Lade Nachrichten…</div>
</div>
</div>
<div class="admin-section">
<h2>🚫 Gesperrte Nutzer</h2>
<div id="adminBannedContainer" class="admin-table-wrapper">
<div class="admin-empty-state">Lade Nutzer…</div>
</div>
</div>
</div>
</div>
<?php elseif ($isAdminPage): ?>
<div class="admin-login-container">
<h1>🔐 Admin-Login</h1>
<p>Zugriff nur für autorisierte Moderatoren.</p>
<div class="admin-error-message" id="adminError"></div>
<form id="adminLoginForm">
<div class="form-group">
<label for="adminUsername">Benutzername</label>
<input type="text" id="adminUsername" autocomplete="username" required>
</div>
<div class="form-group">
<label for="adminPassword">Passwort</label>
<input type="password" id="adminPassword" autocomplete="current-password" required>
</div>
<button type="submit">Anmelden</button>
</form>
<div class="back-link">
<a href="?">Zurück zum Chat</a>
</div>
</div>
<?php elseif (!isLoggedIn()): ?>
<!-- REGISTRATION FORM --> <!-- REGISTRATION FORM -->
<div class="register-container"> <div class="auth-container">
<h1>💬 Secure Private Chat</h1> <h1>💬 Secure Private Chat</h1>
<p class="subtitle">Sicherer Chat mit Altersverifikation</p> <p class="subtitle">Sicherer Chat mit Altersverifikation</p>
@@ -1943,25 +2279,335 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
// JAVASCRIPT // JAVASCRIPT
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
<?php if (!isLoggedIn()): ?> <?php if (isAdmin()): ?>
// ADMIN DASHBOARD
const adminStatsGrid = document.getElementById('adminStatsGrid');
const adminReportsContainer = document.getElementById('adminReportsContainer');
const adminFlaggedContainer = document.getElementById('adminFlaggedContainer');
const adminBannedContainer = document.getElementById('adminBannedContainer');
function adminEscapeHtml(text) {
const div = document.createElement('div');
div.textContent = text ?? '';
return div.innerHTML;
}
function adminFormatDate(value) {
if (!value) return '-';
try {
return new Date(value).toLocaleString('de-DE');
} catch (e) {
return value;
}
}
async function adminFetch(action, payload = {}) {
const formData = new FormData();
formData.append('action', action);
Object.entries(payload).forEach(([key, val]) => formData.append(key, val));
const response = await fetch('', { method: 'POST', body: formData });
return response.json();
}
function renderAdminStats(stats) {
if (!stats) {
adminStatsGrid.innerHTML = '<div class="admin-empty-state">Keine Statistiken verfügbar.</div>';
return;
}
const statItems = [
['Registrierte Nutzer', stats.total_users],
['U18 Nutzer', stats.u18_users],
['Ü18 Nutzer', stats.o18_users],
['Aktiv online', stats.online_users],
['Nachrichten gesamt', stats.total_messages],
['Markierte Nachrichten', stats.flagged_messages],
['Offene Meldungen', stats.pending_reports],
['Gesperrte Nutzer', stats.banned_users]
];
adminStatsGrid.innerHTML = statItems.map(([label, value]) => `
<div class="admin-stat-card">
<span>${adminEscapeHtml(label)}</span>
<strong>${Number(value) || 0}</strong>
</div>
`).join('');
}
function renderReports(reports) {
if (!reports || reports.length === 0) {
adminReportsContainer.innerHTML = '<div class="admin-empty-state">Aktuell liegen keine offenen Meldungen vor.</div>';
return;
}
const rows = reports.map(report => `
<tr data-report-id="${report.id}" data-user-id="${report.reported_user_id}">
<td>${adminEscapeHtml(report.reporter)}</td>
<td>${adminEscapeHtml(report.reported)}</td>
<td>
<strong>${adminEscapeHtml(report.reason)}</strong>
${report.message ? `<div>${adminEscapeHtml(report.message)}</div>` : ''}
</td>
<td>${adminEscapeHtml(adminFormatDate(report.timestamp))}</td>
<td>
<div class="admin-action-buttons">
<button class="btn-danger" data-action="ban" data-user="${report.reported_user_id}">Sperren</button>
<button class="btn-success" data-action="resolve" data-report="${report.id}">Erledigt</button>
</div>
</td>
</tr>
`).join('');
adminReportsContainer.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>Melder</th>
<th>Gemeldeter</th>
<th>Grund &amp; Nachricht</th>
<th>Zeitpunkt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
`;
adminReportsContainer.querySelectorAll('button[data-action="ban"]').forEach(btn => {
btn.addEventListener('click', () => adminBanUser(btn.dataset.user));
});
adminReportsContainer.querySelectorAll('button[data-action="resolve"]').forEach(btn => {
btn.addEventListener('click', () => adminResolveReport(btn.dataset.report));
});
}
function renderFlagged(flagged) {
if (!flagged || flagged.length === 0) {
adminFlaggedContainer.innerHTML = '<div class="admin-empty-state">Keine markierten Nachrichten vorhanden.</div>';
return;
}
const rows = flagged.map(item => `
<tr data-message-id="${item.id}" data-user-id="${item.user_id}">
<td>${adminEscapeHtml(item.user)}</td>
<td>${adminEscapeHtml(item.message)}</td>
<td>${adminEscapeHtml(item.reason)}</td>
<td>${adminEscapeHtml(adminFormatDate(item.timestamp))}</td>
<td>
<div class="admin-action-buttons">
<button class="btn-danger" data-action="ban" data-user="${item.user_id}">Sperren</button>
<button class="btn-secondary" data-action="delete" data-message="${item.id}">Löschen</button>
</div>
</td>
</tr>
`).join('');
adminFlaggedContainer.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>Nutzer</th>
<th>Nachricht</th>
<th>Grund</th>
<th>Zeitpunkt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
`;
adminFlaggedContainer.querySelectorAll('button[data-action="ban"]').forEach(btn => {
btn.addEventListener('click', () => adminBanUser(btn.dataset.user));
});
adminFlaggedContainer.querySelectorAll('button[data-action="delete"]').forEach(btn => {
btn.addEventListener('click', () => adminDeleteMessage(btn.dataset.message));
});
}
function renderBanned(banned) {
if (!banned || banned.length === 0) {
adminBannedContainer.innerHTML = '<div class="admin-empty-state">Keine Nutzer gesperrt.</div>';
return;
}
const rows = banned.map(user => `
<tr data-user-id="${user.id}">
<td>${adminEscapeHtml(user.display_name)}</td>
<td>${adminEscapeHtml(user.reason)}</td>
<td>${adminEscapeHtml(adminFormatDate(user.last_seen))}</td>
<td>
<div class="admin-action-buttons">
<button class="btn-success" data-action="unban" data-user="${user.id}">Entsperren</button>
</div>
</td>
</tr>
`).join('');
adminBannedContainer.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>Nutzer</th>
<th>Grund</th>
<th>Zuletzt aktiv</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
`;
adminBannedContainer.querySelectorAll('button[data-action="unban"]').forEach(btn => {
btn.addEventListener('click', () => adminUnbanUser(btn.dataset.user));
});
}
async function loadAdminStats() {
const response = await fetch('?action=admin_get_stats');
const result = await response.json();
if (result.success) {
renderAdminStats(result.stats);
}
}
async function loadAdminReports() {
const response = await fetch('?action=admin_get_reports');
const result = await response.json();
if (result.success) {
renderReports(result.reports);
}
}
async function loadAdminFlagged() {
const response = await fetch('?action=admin_get_flagged');
const result = await response.json();
if (result.success) {
renderFlagged(result.flagged);
}
}
async function loadAdminBanned() {
const result = await fetch('?action=admin_get_banned_users');
const data = await result.json();
if (data.success) {
renderBanned(data.banned);
}
}
async function adminBanUser(userId) {
const reason = prompt('Grund für die Sperre eingeben:', 'Verstoß gegen Nutzungsbedingungen');
if (reason === null) return;
const result = await adminFetch('admin_ban_user', { user_id: userId, reason });
if (!result.success) {
alert(result.error || 'Aktion fehlgeschlagen');
return;
}
await refreshAdminData();
}
async function adminResolveReport(reportId) {
const actionTaken = prompt('Status für Report festlegen (z.B. resolved, dismissed):', 'resolved');
if (actionTaken === null) return;
const result = await adminFetch('admin_resolve_report', { report_id: reportId, action_taken: actionTaken });
if (!result.success) {
alert(result.error || 'Aktion fehlgeschlagen');
return;
}
await refreshAdminData();
}
async function adminDeleteMessage(messageId) {
if (!confirm('Markierte Nachricht wirklich löschen?')) return;
const result = await adminFetch('admin_delete_message', { message_id: messageId });
if (!result.success) {
alert(result.error || 'Aktion fehlgeschlagen');
return;
}
await refreshAdminData();
}
async function adminUnbanUser(userId) {
const result = await adminFetch('admin_unban_user', { user_id: userId });
if (!result.success) {
alert(result.error || 'Aktion fehlgeschlagen');
return;
}
await refreshAdminData();
}
async function refreshAdminData() {
await Promise.all([
loadAdminStats(),
loadAdminReports(),
loadAdminFlagged(),
loadAdminBanned()
]);
}
document.getElementById('adminLogoutBtn').addEventListener('click', async () => {
await adminFetch('logout');
window.location.href = '?';
});
refreshAdminData();
setInterval(refreshAdminData, 30000);
<?php elseif ($isAdminPage): ?>
// ADMIN LOGIN
const adminLoginForm = document.getElementById('adminLoginForm');
const adminError = document.getElementById('adminError');
adminLoginForm.addEventListener('submit', async (e) => {
e.preventDefault();
adminError.style.display = 'none';
const formData = new FormData();
formData.append('action', 'admin_login');
formData.append('username', document.getElementById('adminUsername').value.trim());
formData.append('password', document.getElementById('adminPassword').value);
try {
const response = await fetch('', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
window.location.href = '?admin=1';
} else {
adminError.textContent = result.error || 'Anmeldung fehlgeschlagen';
adminError.style.display = 'block';
}
} catch (error) {
adminError.textContent = 'Server nicht erreichbar';
adminError.style.display = 'block';
}
});
<?php elseif (!isLoggedIn()): ?>
// REGISTRATION // REGISTRATION
document.getElementById('registerForm').addEventListener('submit', async (e) => { document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const username = document.getElementById('username').value.trim(); const username = document.getElementById('username').value.trim();
const birthdate = document.getElementById('birthdate').value; const birthdate = document.getElementById('birthdate').value;
const agreedTerms = document.getElementById('agreeTerms').checked; const agreedTerms = document.getElementById('agreeTerms').checked;
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'register'); formData.append('action', 'register');
formData.append('username', username); formData.append('username', username);
formData.append('birthdate', birthdate); formData.append('birthdate', birthdate);
formData.append('agreed_terms', agreedTerms); formData.append('agreed_terms', agreedTerms);
try { try {
const response = await fetch('', { method: 'POST', body: formData }); const response = await fetch('', { method: 'POST', body: formData });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
window.location.reload(); window.location.reload();
} else { } else {
@@ -1986,24 +2632,22 @@ const state = {
eventSource: null eventSource: null
}; };
// Load Users
async function loadUsers() { async function loadUsers() {
const response = await fetch('?action=get_users'); const response = await fetch('?action=get_users');
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
state.users = result.users; state.users = result.users;
renderUserList(); renderUserList();
} }
} }
// Render User List
function renderUserList() { function renderUserList() {
const userList = document.getElementById('userList'); const userList = document.getElementById('userList');
const searchTerm = document.getElementById('userSearch').value.toLowerCase(); const searchTerm = document.getElementById('userSearch').value.toLowerCase();
const filtered = state.users.filter(u => u.display_name.toLowerCase().includes(searchTerm)); const filtered = state.users.filter(u => u.display_name.toLowerCase().includes(searchTerm));
userList.innerHTML = filtered.map(user => ` userList.innerHTML = filtered.map(user => `
<div class="user-item ${user.id === state.selectedUserId ? 'active' : ''}" onclick="selectUser(${user.id}, '${user.display_name}')"> <div class="user-item ${user.id === state.selectedUserId ? 'active' : ''}" onclick="selectUser(${user.id}, '${user.display_name}')">
<div class="user-avatar"> <div class="user-avatar">
@@ -2019,46 +2663,43 @@ function renderUserList() {
`).join(''); `).join('');
} }
// Select User
function selectUser(userId, displayName) { function selectUser(userId, displayName) {
state.selectedUserId = userId; state.selectedUserId = userId;
document.getElementById('chatWelcome').style.display = 'none'; document.getElementById('chatWelcome').style.display = 'none';
document.getElementById('chatMessagesContainer').style.display = 'flex'; document.getElementById('chatMessagesContainer').style.display = 'flex';
document.getElementById('chatMessagesHeader').innerHTML = ` document.getElementById('chatMessagesHeader').innerHTML = `
<div class="user-avatar">${displayName.charAt(0).toUpperCase()}</div> <div class="user-avatar">${displayName.charAt(0).toUpperCase()}</div>
<div><div class="user-name">${displayName}</div></div> <div><div class="user-name">${displayName}</div></div>
`; `;
loadMessages(userId); loadMessages(userId);
renderUserList(); renderUserList();
} }
// Load Messages
async function loadMessages(userId) { async function loadMessages(userId) {
const response = await fetch(`?action=get_messages&user_id=${userId}`); const response = await fetch(`?action=get_messages&user_id=${userId}`);
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
state.messages = result.messages; state.messages = result.messages;
renderMessages(); renderMessages();
markAsRead(userId); markAsRead(userId);
if (result.messages.length > 0) { if (result.messages.length > 0) {
state.lastMessageId = Math.max(...result.messages.map(m => m.id)); state.lastMessageId = Math.max(...result.messages.map(m => m.id));
} }
} }
} }
// Render Messages
function renderMessages() { function renderMessages() {
const container = document.getElementById('chatMessages'); const container = document.getElementById('chatMessages');
container.innerHTML = state.messages.map(msg => { container.innerHTML = state.messages.map(msg => {
const isSent = msg.from_user_id === state.currentUserId; const isSent = msg.from_user_id === state.currentUserId;
const time = new Date(msg.timestamp).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); const time = new Date(msg.timestamp).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
return ` return `
<div class="message ${isSent ? 'message-sent' : 'message-received'}"> <div class="message ${isSent ? 'message-sent' : 'message-received'}">
<div class="message-text">${escapeHtml(msg.message)}</div> <div class="message-text">${escapeHtml(msg.message)}</div>
@@ -2066,25 +2707,24 @@ function renderMessages() {
</div> </div>
`; `;
}).join(''); }).join('');
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
} }
// Send Message
async function sendMessage() { async function sendMessage() {
const input = document.getElementById('chatInput'); const input = document.getElementById('chatInput');
const message = input.value.trim(); const message = input.value.trim();
if (!message || !state.selectedUserId) return; if (!message || !state.selectedUserId) return;
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'send_message'); formData.append('action', 'send_message');
formData.append('to_user_id', state.selectedUserId); formData.append('to_user_id', state.selectedUserId);
formData.append('message', message); formData.append('message', message);
const response = await fetch('', { method: 'POST', body: formData }); const response = await fetch('', { method: 'POST', body: formData });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
input.value = ''; input.value = '';
} else { } else {
@@ -2092,36 +2732,34 @@ async function sendMessage() {
} }
} }
// Mark as Read
async function markAsRead(userId) { async function markAsRead(userId) {
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'mark_read'); formData.append('action', 'mark_read');
formData.append('user_id', userId); formData.append('user_id', userId);
await fetch('', { method: 'POST', body: formData }); await fetch('', { method: 'POST', body: formData });
loadUsers(); loadUsers();
} }
// SSE
function startSSE() { function startSSE() {
state.eventSource = new EventSource(`?stream=events&last_message_id=${state.lastMessageId}`); state.eventSource = new EventSource(`?stream=events&last_message_id=${state.lastMessageId}`);
state.eventSource.onmessage = (event) => { state.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.type === 'messages' && data.messages) { if (data.type === 'messages' && data.messages) {
data.messages.forEach(msg => { data.messages.forEach(msg => {
if (msg.id > state.lastMessageId) { if (msg.id > state.lastMessageId) {
state.lastMessageId = msg.id; state.lastMessageId = msg.id;
if (state.selectedUserId && if (state.selectedUserId &&
((msg.from_user_id === state.selectedUserId && msg.to_user_id === state.currentUserId) || ((msg.from_user_id === state.selectedUserId && msg.to_user_id === state.currentUserId) ||
(msg.from_user_id === state.currentUserId && msg.to_user_id === state.selectedUserId))) { (msg.from_user_id === state.currentUserId && msg.to_user_id === state.selectedUserId))) {
if (!state.messages.find(m => m.id === msg.id)) { if (!state.messages.find(m => m.id === msg.id)) {
state.messages.push(msg); state.messages.push(msg);
renderMessages(); renderMessages();
if (msg.to_user_id === state.currentUserId) { if (msg.to_user_id === state.currentUserId) {
markAsRead(msg.from_user_id); markAsRead(msg.from_user_id);
} }
@@ -2129,20 +2767,18 @@ function startSSE() {
} }
} }
}); });
loadUsers(); loadUsers();
} }
}; };
} }
// Utility
function escapeHtml(text) { function escapeHtml(text) {
const div = document.createElement('div'); const div = document.createElement('div');
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
// Event Listeners
document.getElementById('sendButton').addEventListener('click', sendMessage); document.getElementById('sendButton').addEventListener('click', sendMessage);
document.getElementById('chatInput').addEventListener('keypress', (e) => { document.getElementById('chatInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
@@ -2158,20 +2794,17 @@ document.getElementById('logoutBtn').addEventListener('click', async () => {
window.location.reload(); window.location.reload();
}); });
// Auto-resize textarea
document.getElementById('chatInput').addEventListener('input', function() { document.getElementById('chatInput').addEventListener('input', function() {
this.style.height = 'auto'; this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 100) + 'px'; this.style.height = Math.min(this.scrollHeight, 100) + 'px';
}); });
// Ping every 10s
setInterval(async () => { setInterval(async () => {
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'ping'); formData.append('action', 'ping');
await fetch('', { method: 'POST', body: formData }); await fetch('', { method: 'POST', body: formData });
}, 10000); }, 10000);
// Init
loadUsers(); loadUsers();
startSSE(); startSSE();
setInterval(loadUsers, 30000); setInterval(loadUsers, 30000);