diff --git a/write.php b/write.php
index cba271d..a034c8b 100644
--- a/write.php
+++ b/write.php
@@ -28,7 +28,8 @@ define('DB_FILE', __DIR__ . '/chat_secure.db');
define('MESSAGE_RETENTION_HOURS', 24);
define('LOG_RETENTION_MONTHS', 6);
define('ONLINE_TIMEOUT_SECONDS', 30);
-define('SSE_RETRY_MS', 1000);
+define('SSE_RETRY_MS', 500);
+define('MAX_MESSAGES_PER_FETCH', 200);
// Rate Limiting
define('MAX_MESSAGES_PER_MINUTE', 10);
@@ -112,7 +113,17 @@ function getDB() {
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
-
+
+ // User Sessions Table
+ $db->exec('
+ CREATE TABLE IF NOT EXISTS user_sessions (
+ user_id INTEGER PRIMARY KEY,
+ session_token TEXT NOT NULL,
+ last_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ )
+ ');
+
// Reports Table
$db->exec('
CREATE TABLE IF NOT EXISTS reports (
@@ -194,7 +205,8 @@ function getDB() {
$db->exec('CREATE INDEX IF NOT EXISTS idx_users_age_group ON users(age_group)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_blocks ON blocks(blocker_id, blocked_id)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status)');
-
+ $db->exec('CREATE INDEX IF NOT EXISTS idx_user_sessions_last_seen ON user_sessions(last_seen)');
+
return $db;
}
@@ -382,6 +394,9 @@ function cleanupOldData() {
// Delete old logs (keep 6 months)
$months = LOG_RETENTION_MONTHS;
$db->exec("DELETE FROM security_logs WHERE timestamp < datetime('now', '-{$months} months')");
+
+ // Remove stale session placeholders
+ $db->exec("DELETE FROM user_sessions WHERE last_seen < datetime('now', '-5 minutes')");
}
// ═══════════════════════════════════════════════════════════
@@ -424,10 +439,117 @@ function updateOnlineStatus($userId) {
');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
-
+
$stmt = $db->prepare('UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE id = :user_id');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
+
+ touchUserSession($userId);
+}
+
+function generateSessionToken() {
+ return bin2hex(random_bytes(32));
+}
+
+function startUserSession($userId) {
+ if (!$userId) {
+ return ['allowed' => false, 'error' => 'Ungültige Benutzer-ID'];
+ }
+
+ $db = getDB();
+ $stmt = $db->prepare('SELECT session_token, last_seen FROM user_sessions WHERE user_id = :user_id');
+ $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
+ $result = $stmt->execute();
+ $existing = $result->fetchArray(SQLITE3_ASSOC);
+
+ if ($existing && !empty($existing['last_seen'])) {
+ $secondsSinceLastSeen = time() - strtotime($existing['last_seen']);
+
+ if ($secondsSinceLastSeen < ONLINE_TIMEOUT_SECONDS) {
+ return [
+ 'allowed' => false,
+ 'error' => 'Du bist bereits auf einem anderen Gerät eingeloggt. Bitte dort zuerst ausloggen oder kurz warten.'
+ ];
+ }
+ }
+
+ $token = generateSessionToken();
+
+ $stmt = $db->prepare('
+ INSERT OR REPLACE INTO user_sessions (user_id, session_token, last_seen)
+ VALUES (:user_id, :token, CURRENT_TIMESTAMP)
+ ');
+ $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
+ $stmt->bindValue(':token', $token, SQLITE3_TEXT);
+ $stmt->execute();
+
+ $_SESSION['session_token'] = $token;
+
+ return ['allowed' => true, 'token' => $token];
+}
+
+function touchUserSession($userId) {
+ if (!$userId || empty($_SESSION['session_token'])) {
+ return;
+ }
+
+ $db = getDB();
+ $stmt = $db->prepare('
+ UPDATE user_sessions
+ SET last_seen = CURRENT_TIMESTAMP
+ WHERE user_id = :user_id AND session_token = :token
+ ');
+ $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
+ $stmt->bindValue(':token', $_SESSION['session_token'], SQLITE3_TEXT);
+ $stmt->execute();
+}
+
+function clearUserSession($userId) {
+ if (!$userId) {
+ return;
+ }
+
+ $db = getDB();
+ $stmt = $db->prepare('DELETE FROM user_sessions WHERE user_id = :user_id');
+ $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
+ $stmt->execute();
+
+ unset($_SESSION['session_token']);
+}
+
+function validateActiveSession() {
+ if (!isLoggedIn()) {
+ return false;
+ }
+
+ $token = $_SESSION['session_token'] ?? null;
+ if (!$token) {
+ return false;
+ }
+
+ $db = getDB();
+ $stmt = $db->prepare('SELECT session_token, last_seen FROM user_sessions WHERE user_id = :user_id');
+ $stmt->bindValue(':user_id', getCurrentUserId(), SQLITE3_INTEGER);
+ $result = $stmt->execute();
+ $session = $result->fetchArray(SQLITE3_ASSOC);
+
+ if (!$session) {
+ return false;
+ }
+
+ if (!hash_equals($session['session_token'], $token)) {
+ return false;
+ }
+
+ if (!empty($session['last_seen'])) {
+ $secondsSinceLastSeen = time() - strtotime($session['last_seen']);
+
+ if ($secondsSinceLastSeen > ONLINE_TIMEOUT_SECONDS * 3) {
+ return false;
+ }
+ }
+
+ return true;
}
// ═══════════════════════════════════════════════════════════
@@ -512,9 +634,19 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
$stmt->bindValue(':birthdate', $birthdate, SQLITE3_TEXT);
$stmt->bindValue(':age_group', $ageGroup, SQLITE3_TEXT);
$stmt->execute();
-
+
$dbUserId = $db->lastInsertRowID();
-
+
+ $sessionResult = startUserSession($dbUserId);
+ if (!$sessionResult['allowed']) {
+ logSecurityEvent($dbUserId, 'LOGIN_BLOCKED_DUPLICATE_SESSION', 'REGISTER');
+ echo json_encode([
+ 'success' => false,
+ 'error' => $sessionResult['error']
+ ]);
+ exit;
+ }
+
$_SESSION['user_id'] = $dbUserId;
$_SESSION['username'] = $username;
$_SESSION['user_display_id'] = $userId;
@@ -567,18 +699,32 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
// ───────────────────────────────────────────────────────
if ($action === 'logout') {
if (isLoggedIn()) {
- logSecurityEvent(getCurrentUserId(), 'LOGOUT', '');
+ $currentUserId = getCurrentUserId();
+ logSecurityEvent($currentUserId, 'LOGOUT', '');
+ clearUserSession($currentUserId);
}
session_destroy();
echo json_encode(['success' => true]);
exit;
}
-
+
// All other actions require login
if (!isLoggedIn() && !isAdmin()) {
echo json_encode(['success' => false, 'error' => 'Nicht eingeloggt']);
exit;
}
+
+ if (isLoggedIn() && !isAdmin() && !validateActiveSession()) {
+ $userId = getCurrentUserId();
+ clearUserSession($userId);
+ session_destroy();
+ echo json_encode(['success' => false, 'error' => 'Deine Sitzung ist nicht mehr gültig. Bitte erneut einloggen.']);
+ exit;
+ }
+
+ if (isLoggedIn() && !isAdmin()) {
+ touchUserSession(getCurrentUserId());
+ }
// ───────────────────────────────────────────────────────
// PING (UPDATE ONLINE STATUS)
@@ -714,28 +860,33 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
}
$query = '
- SELECT
- m.id,
- m.from_user_id,
- m.to_user_id,
- m.message,
- m.timestamp,
- m.is_read,
- m.is_flagged,
- u.username as from_username,
- u.user_id as from_display_id
- FROM messages m
- JOIN users u ON m.from_user_id = u.id
- WHERE
- (m.from_user_id = :current_user_id AND m.to_user_id = :other_user_id)
- OR
- (m.from_user_id = :other_user_id AND m.to_user_id = :current_user_id)
- ORDER BY m.timestamp ASC
+ SELECT * FROM (
+ SELECT
+ m.id,
+ m.from_user_id,
+ m.to_user_id,
+ m.message,
+ m.timestamp,
+ m.is_read,
+ m.is_flagged,
+ u.username as from_username,
+ u.user_id as from_display_id
+ FROM messages m
+ JOIN users u ON m.from_user_id = u.id
+ WHERE
+ (m.from_user_id = :current_user_id AND m.to_user_id = :other_user_id)
+ OR
+ (m.from_user_id = :other_user_id AND m.to_user_id = :current_user_id)
+ ORDER BY m.id DESC
+ LIMIT :limit
+ )
+ ORDER BY id ASC
';
-
+
$stmt = $db->prepare($query);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER);
+ $stmt->bindValue(':limit', MAX_MESSAGES_PER_FETCH, SQLITE3_INTEGER);
$result = $stmt->execute();
$messages = [];
@@ -1373,16 +1524,22 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
if (!isLoggedIn()) {
exit;
}
-
+
+ if (!validateActiveSession()) {
+ exit;
+ }
+
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
-
+
$currentUserId = getCurrentUserId();
$currentAgeGroup = getCurrentAgeGroup();
$lastMessageId = intval($_GET['last_message_id'] ?? 0);
-
+
+ touchUserSession($currentUserId);
+
set_time_limit(0);
ob_implicit_flush(true);
while (ob_get_level() > 0) {
@@ -1461,6 +1618,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
+
💬 Secure Private Chat
@@ -2430,6 +2632,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
+
@@ -2798,72 +3001,302 @@ const state = {
users: [],
messages: [],
lastMessageId: 0,
- eventSource: null
+ eventSource: null,
+ isLoadingUsers: false,
+ isLoadingMessages: false,
+ connectionErrorShown: false
};
-async function loadUsers() {
- const response = await fetch('?action=get_users');
- const result = await response.json();
+const userListEl = document.getElementById('userList');
+const userSearchInput = document.getElementById('userSearch');
+const chatWelcomeEl = document.getElementById('chatWelcome');
+const chatMessagesContainerEl = document.getElementById('chatMessagesContainer');
+const chatMessagesEl = document.getElementById('chatMessages');
+const chatStateMessageEl = document.getElementById('chatStateMessage');
+const chatMessagesHeaderEl = document.getElementById('chatMessagesHeader');
+const chatInputEl = document.getElementById('chatInput');
+const sendButtonEl = document.getElementById('sendButton');
+let messageAbortController = null;
- if (result.success) {
- state.users = result.users;
+async function loadUsers() {
+ if (!userListEl) {
+ return;
+ }
+
+ state.isLoadingUsers = true;
+ renderUserList();
+
+ try {
+ const response = await fetch('?action=get_users');
+ if (!response.ok) {
+ throw new Error('NETZWERK_FEHLER');
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ state.users = Array.isArray(result.users) ? result.users : [];
+ } else {
+ throw new Error(result.error || 'Nutzerliste konnte nicht geladen werden.');
+ }
+
+ state.isLoadingUsers = false;
renderUserList();
+ } catch (error) {
+ console.error('Nutzerliste konnte nicht geladen werden:', error);
+ state.isLoadingUsers = false;
+ if (userListEl) {
+ userListEl.innerHTML = 'Nutzerliste konnte nicht geladen werden.
';
+ }
}
}
function renderUserList() {
- const userList = document.getElementById('userList');
- const searchTerm = document.getElementById('userSearch').value.toLowerCase();
+ if (!userListEl) {
+ return;
+ }
- const filtered = state.users.filter(u => u.display_name.toLowerCase().includes(searchTerm));
+ const searchTerm = (userSearchInput?.value || '').toLowerCase();
+ const users = Array.isArray(state.users) ? state.users : [];
- userList.innerHTML = filtered.map(user => `
-
-
- ${user.username.charAt(0).toUpperCase()}
-
-
-
-
${user.display_name}
-
${user.is_online ? 'Online' : 'Offline'}
-
- ${user.unread_count > 0 ? `
${user.unread_count}
` : ''}
-
- `).join('');
+ if (state.isLoadingUsers && users.length === 0) {
+ userListEl.innerHTML = 'Nutzer werden geladen…
';
+ return;
+ }
+
+ if (users.length === 0) {
+ userListEl.innerHTML = 'Noch keine passenden Kontakte verfügbar.
';
+ return;
+ }
+
+ const filtered = users.filter(user => user.display_name.toLowerCase().includes(searchTerm));
+
+ if (filtered.length === 0) {
+ userListEl.innerHTML = 'Keine Treffer für deine Suche.
';
+ return;
+ }
+
+ const offlineLimit = 5;
+ const onlineUsers = [];
+ const offlineUsers = [];
+
+ filtered.forEach(user => {
+ if (user.is_online) {
+ onlineUsers.push(user);
+ } else {
+ offlineUsers.push(user);
+ }
+ });
+
+ const limitedUsers = onlineUsers.concat(offlineUsers.slice(0, offlineLimit));
+
+ const fragment = document.createDocumentFragment();
+
+ limitedUsers.forEach(user => {
+ const item = document.createElement('button');
+ item.type = 'button';
+ item.className = 'user-item' + (Number(user.id) === Number(state.selectedUserId) ? ' active' : '');
+ item.dataset.userId = String(user.id);
+ item.dataset.displayName = user.display_name;
+
+ const avatar = document.createElement('div');
+ avatar.className = 'user-avatar';
+ avatar.textContent = (user.username || '?').charAt(0).toUpperCase();
+
+ const indicator = document.createElement('div');
+ indicator.className = 'online-indicator' + (user.is_online ? '' : ' offline-indicator');
+ avatar.appendChild(indicator);
+
+ const infoWrapper = document.createElement('div');
+ infoWrapper.className = 'user-info-text';
+
+ const name = document.createElement('div');
+ name.className = 'user-name';
+ name.textContent = user.display_name;
+
+ const status = document.createElement('div');
+ status.className = 'user-status';
+ status.textContent = user.is_online ? 'Online' : 'Offline';
+
+ infoWrapper.appendChild(name);
+ infoWrapper.appendChild(status);
+
+ item.appendChild(avatar);
+ item.appendChild(infoWrapper);
+
+ if (Number(user.unread_count) > 0) {
+ const unread = document.createElement('div');
+ unread.className = 'unread-badge';
+ unread.textContent = String(user.unread_count);
+ item.appendChild(unread);
+ }
+
+ item.addEventListener('click', () => {
+ selectUser(Number(user.id), user.display_name);
+ });
+
+ fragment.appendChild(item);
+ });
+
+ userListEl.innerHTML = '';
+ userListEl.appendChild(fragment);
+
+ if (offlineUsers.length > offlineLimit) {
+ const hint = document.createElement('div');
+ hint.className = 'user-status';
+ hint.style.textAlign = 'center';
+ hint.style.marginTop = '12px';
+ hint.textContent = 'Weitere Offline-Nutzer werden ausgeblendet.';
+ userListEl.appendChild(hint);
+ }
+}
+
+function renderChatHeader(displayName) {
+ if (!chatMessagesHeaderEl) {
+ return;
+ }
+
+ chatMessagesHeaderEl.innerHTML = '';
+
+ const avatar = document.createElement('div');
+ avatar.className = 'user-avatar';
+ const initial = (displayName?.trim() || '?').charAt(0).toUpperCase();
+ avatar.textContent = initial || '?';
+
+ const info = document.createElement('div');
+ const name = document.createElement('div');
+ name.className = 'user-name';
+ name.textContent = displayName;
+ info.appendChild(name);
+
+ chatMessagesHeaderEl.appendChild(avatar);
+ chatMessagesHeaderEl.appendChild(info);
+}
+
+function updateChatState(type, message = '') {
+ if (!chatStateMessageEl || !chatMessagesEl) {
+ return;
+ }
+
+ chatStateMessageEl.className = 'chat-state-message';
+
+ if (!type) {
+ chatStateMessageEl.textContent = '';
+ chatStateMessageEl.classList.add('hidden');
+ chatMessagesEl.classList.remove('hidden');
+ return;
+ }
+
+ chatStateMessageEl.textContent = message;
+ chatStateMessageEl.classList.remove('hidden');
+
+ if (type === 'loading') {
+ chatStateMessageEl.classList.add('loading-state');
+ } else if (type === 'error') {
+ chatStateMessageEl.classList.add('error-state');
+ } else if (type === 'empty') {
+ chatStateMessageEl.classList.add('empty-messages');
+ }
+
+ const hideMessages = type === 'loading' || type === 'error' || type === 'empty';
+ chatMessagesEl.classList.toggle('hidden', hideMessages);
}
function selectUser(userId, displayName) {
state.selectedUserId = userId;
+ state.messages = [];
- document.getElementById('chatWelcome').style.display = 'none';
- document.getElementById('chatMessagesContainer').style.display = 'flex';
+ if (chatWelcomeEl) {
+ chatWelcomeEl.style.display = 'none';
+ }
- document.getElementById('chatMessagesHeader').innerHTML = `
- ${displayName.charAt(0).toUpperCase()}
-
- `;
+ if (chatMessagesContainerEl) {
+ chatMessagesContainerEl.style.display = 'flex';
+ }
- loadMessages(userId);
+ if (chatMessagesEl) {
+ chatMessagesEl.innerHTML = '';
+ chatMessagesEl.classList.add('hidden');
+ }
+
+ renderChatHeader(displayName);
+ updateChatState('loading', 'Nachrichten werden geladen…');
renderUserList();
+ loadMessages(userId);
}
async function loadMessages(userId) {
- const response = await fetch(`?action=get_messages&user_id=${userId}`);
- const result = await response.json();
+ if (!userId) {
+ return;
+ }
- if (result.success) {
- state.messages = result.messages;
- renderMessages();
- markAsRead(userId);
+ if (messageAbortController) {
+ messageAbortController.abort();
+ }
- if (result.messages.length > 0) {
- state.lastMessageId = Math.max(...result.messages.map(m => m.id));
+ const currentController = new AbortController();
+ messageAbortController = currentController;
+
+ state.isLoadingMessages = true;
+ updateChatState('loading', 'Nachrichten werden geladen…');
+
+ try {
+ const response = await fetch(`?action=get_messages&user_id=${userId}`, { signal: currentController.signal });
+ if (!response.ok) {
+ throw new Error('NETZWERK_FEHLER');
+ }
+
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Nachrichten konnten nicht geladen werden.');
+ }
+
+ state.messages = Array.isArray(result.messages) ? result.messages : [];
+
+ if (state.messages.length > 0) {
+ renderMessages();
+ markAsRead(userId);
+ const newLastMessageId = Math.max(...state.messages.map(m => Number(m.id)));
+ state.lastMessageId = Math.max(state.lastMessageId, newLastMessageId);
+ } else {
+ if (chatMessagesEl) {
+ chatMessagesEl.innerHTML = '';
+ }
+ updateChatState('empty', 'Noch keine Nachrichten. Starte das Gespräch!');
+ }
+ } catch (error) {
+ if (error && error.name === 'AbortError') {
+ return;
+ }
+ console.error('Nachrichten konnten nicht geladen werden:', error);
+ state.messages = [];
+ if (chatMessagesEl) {
+ chatMessagesEl.innerHTML = '';
+ }
+ const errorMessage = (error && error.message && error.message !== 'NETZWERK_FEHLER')
+ ? error.message
+ : 'Nachrichten konnten nicht geladen werden. Bitte versuche es erneut.';
+ updateChatState('error', errorMessage);
+ } finally {
+ if (messageAbortController === currentController) {
+ messageAbortController = null;
+ state.isLoadingMessages = false;
}
}
}
function renderMessages() {
- const container = document.getElementById('chatMessages');
+ const container = chatMessagesEl;
+
+ if (!container) {
+ return;
+ }
+
+ if (!Array.isArray(state.messages) || state.messages.length === 0) {
+ container.innerHTML = '';
+ return;
+ }
container.innerHTML = state.messages.map(msg => {
const isSent = msg.from_user_id === state.currentUserId;
@@ -2877,12 +3310,17 @@ function renderMessages() {
`;
}).join('');
+ container.classList.remove('hidden');
container.scrollTop = container.scrollHeight;
+ updateChatState(null);
}
async function sendMessage() {
- const input = document.getElementById('chatInput');
- const message = input.value.trim();
+ if (!chatInputEl) {
+ return;
+ }
+
+ const message = chatInputEl.value.trim();
if (!message || !state.selectedUserId) return;
@@ -2895,7 +3333,8 @@ async function sendMessage() {
const result = await response.json();
if (result.success) {
- input.value = '';
+ chatInputEl.value = '';
+ chatInputEl.dispatchEvent(new Event('input'));
} else {
alert(result.error);
}
@@ -2911,21 +3350,47 @@ async function markAsRead(userId) {
}
function startSSE() {
- state.eventSource = new EventSource(`?stream=events&last_message_id=${state.lastMessageId}`);
+ if (state.eventSource) {
+ state.eventSource.close();
+ }
+
+ const url = `?stream=events&last_message_id=${state.lastMessageId}&t=${Date.now()}`;
+ state.eventSource = new EventSource(url);
+
+ state.eventSource.onopen = () => {
+ state.connectionErrorShown = false;
+
+ if (!state.selectedUserId) {
+ return;
+ }
+
+ if (state.isLoadingMessages) {
+ return;
+ }
+
+ if (state.messages.length === 0) {
+ updateChatState('empty', 'Noch keine Nachrichten. Starte das Gespräch!');
+ } else {
+ updateChatState(null);
+ }
+ };
state.eventSource.onmessage = (event) => {
+ state.connectionErrorShown = false;
const data = JSON.parse(event.data);
if (data.type === 'messages' && data.messages) {
data.messages.forEach(msg => {
- if (msg.id > state.lastMessageId) {
- state.lastMessageId = msg.id;
+ const messageId = Number(msg.id);
+
+ if (messageId > state.lastMessageId) {
+ state.lastMessageId = messageId;
if (state.selectedUserId &&
((msg.from_user_id === state.selectedUserId && msg.to_user_id === state.currentUserId) ||
(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 => Number(m.id) === messageId)) {
state.messages.push(msg);
renderMessages();
@@ -2940,6 +3405,22 @@ function startSSE() {
loadUsers();
}
};
+
+ state.eventSource.onerror = () => {
+ if (!state.connectionErrorShown) {
+ state.connectionErrorShown = true;
+ console.warn('SSE-Verbindung unterbrochen, versuche Neuverbindung.');
+ if (state.selectedUserId && !state.isLoadingMessages) {
+ updateChatState('error', 'Live-Verbindung unterbrochen. Erneuter Verbindungsversuch…');
+ }
+ }
+
+ if (state.eventSource) {
+ state.eventSource.close();
+ }
+
+ setTimeout(startSSE, 500);
+ };
}
function escapeHtml(text) {
@@ -2948,22 +3429,25 @@ function escapeHtml(text) {
return div.innerHTML;
}
-document.getElementById('sendButton').addEventListener('click', sendMessage);
-document.getElementById('chatInput').addEventListener('keypress', (e) => {
+sendButtonEl?.addEventListener('click', sendMessage);
+
+chatInputEl?.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
-document.getElementById('userSearch').addEventListener('input', renderUserList);
-document.getElementById('logoutBtn').addEventListener('click', async () => {
+
+userSearchInput?.addEventListener('input', () => renderUserList());
+
+document.getElementById('logoutBtn')?.addEventListener('click', async () => {
const formData = new FormData();
formData.append('action', 'logout');
await fetch('', { method: 'POST', body: formData });
window.location.reload();
});
-document.getElementById('chatInput').addEventListener('input', function() {
+chatInputEl?.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 100) + 'px';
});