ParleyChat/static/app.js

411 lines
No EOL
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

let currentUser = null;
let currentPartner = null;
let selectedUserElement = null;
let lastMessageId = 0;
let messagesInterval = null;
const BASE_PATH = document.querySelector('base')?.getAttribute('href') || '/';
const API_PREFIX = BASE_PATH.replace(/\/$/, '');
// Переключение вкладок логин/регистрация
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.auth-form').forEach(f => f.style.display = 'none');
if (tab === 'login') {
document.querySelectorAll('.tab')[0].classList.add('active');
document.getElementById('login-form').style.display = 'flex';
} else {
document.querySelectorAll('.tab')[1].classList.add('active');
document.getElementById('register-form').style.display = 'flex';
}
}
// Регистрация
async function register() {
const username = document.getElementById('reg-username').value.trim();
const password = document.getElementById('reg-password').value.trim();
if (!username || !password) {
showAuthError('Заполните все поля');
return;
}
try {
const res = await fetch(`${API_PREFIX}/api/register`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password})
});
const data = await res.json();
if (res.ok) {
showAuthError(data.message, 'green');
} else {
showAuthError(data.error);
}
} catch (err) {
showAuthError('Ошибка соединения');
}
}
function showAuthError(message, color = 'red') {
const errorEl = document.getElementById('auth-error');
errorEl.textContent = message;
errorEl.style.color = color;
}
// Вход
async function login() {
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value.trim();
if (!username || !password) {
showAuthError('Заполните все поля');
return;
}
try {
const res = await fetch(`${API_PREFIX}/api/login`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password})
});
const data = await res.json();
if (res.ok) {
currentUser = data;
document.getElementById('auth-screen').style.display = 'none';
document.getElementById('chat-screen').style.display = 'flex';
document.getElementById('current-username').textContent = `👤 ${data.username}`;
if (data.is_admin) {
document.getElementById('admin-btn').style.display = 'block';
}
// На десктопе показываем сайдбар
if (window.innerWidth > 768) {
document.getElementById('sidebar').classList.remove('hidden');
}
loadUsers();
// Запускаем проверку новых сообщений
startPolling();
} else {
showAuthError(data.error);
}
} catch (err) {
showAuthError('Ошибка соединения');
}
}
// Загрузка списка пользователей
async function loadUsers() {
if (!currentUser) return;
try {
const res = await fetch(`${API_PREFIX}/api/users?user_id=${currentUser.user_id}`);
if (!res.ok) {
console.error('Error loading users');
return;
}
const users = await res.json();
const usersList = document.getElementById('users-list');
usersList.innerHTML = '';
if (!users || users.length === 0) {
usersList.innerHTML = '<div style="padding: 20px; text-align: center; color: #888;">Нет активных пользователей</div>';
return;
}
users.forEach(user => {
const userEl = document.createElement('div');
userEl.className = 'user-item';
userEl.innerHTML = `<span class="status"></span><span class="username">${escapeHtml(user.username)}</span>`;
userEl.onclick = () => selectUser(user, userEl);
// Если это текущий партнер, подсвечиваем
if (currentPartner && currentPartner.id === user.id) {
userEl.classList.add('active');
selectedUserElement = userEl;
}
usersList.appendChild(userEl);
});
} catch (err) {
console.error('Error loading users:', err);
}
}
// Выбор собеседника
function selectUser(user, element) {
if (selectedUserElement) {
selectedUserElement.classList.remove('active');
}
currentPartner = user;
selectedUserElement = element;
element.classList.add('active');
document.getElementById('chat-header').innerHTML = `
<span class="mobile-back-btn" onclick="showSidebar()">←</span>
<span>💬 ${escapeHtml(user.username)}</span>
`;
document.getElementById('message-input').disabled = false;
document.getElementById('send-btn').disabled = false;
// Скрываем сайдбар на мобильных
if (window.innerWidth <= 768) {
document.getElementById('sidebar').classList.add('hidden');
}
// Сбрасываем ID последнего сообщения для нового партнера
lastMessageId = 0;
// Загружаем сообщения
loadMessages(true);
}
// Загрузка сообщений с проверкой новых
async function loadMessages(forceScroll = false) {
if (!currentPartner || !currentUser) return;
try {
const res = await fetch(`${API_PREFIX}/api/messages?user_id=${currentUser.user_id}&partner_id=${currentPartner.id}`);
const messages = await res.json();
if (!messages || messages.length === 0) {
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML = `
<div class="empty-chat">
<div>
<div class="empty-chat-icon">💭</div>
<div class="empty-chat-text">Нет сообщений<br>Напишите первое!</div>
</div>
</div>
`;
return;
}
// Проверяем, есть ли новые сообщения
const latestMessage = messages[messages.length - 1];
if (latestMessage.id === lastMessageId && !forceScroll) {
return; // Нет новых сообщений, не обновляем DOM
}
lastMessageId = latestMessage.id;
const messagesDiv = document.getElementById('messages');
// Проверяем, был ли пользователь внизу
const isAtBottom = messagesDiv.scrollTop + messagesDiv.clientHeight >= messagesDiv.scrollHeight - 30;
// Обновляем только если есть изменения
messagesDiv.innerHTML = '';
messages.forEach(msg => {
const msgEl = document.createElement('div');
msgEl.className = `message ${msg.from_id === currentUser.user_id ? 'sent' : 'received'}`;
const time = new Date(msg.timestamp * 1000).toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
});
msgEl.innerHTML = `
<div class="message-content">
${msg.from_id !== currentUser.user_id ? `<div class="message-username">${escapeHtml(msg.username)}</div>` : ''}
${escapeHtml(msg.text)}
<div class="message-time">${time}</div>
</div>
`;
messagesDiv.appendChild(msgEl);
});
// Скроллим вниз только если были внизу или это принудительно
if (isAtBottom || forceScroll) {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
} catch (err) {
console.error('Error loading messages:', err);
}
}
// Защита от XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Отправка сообщения
async function sendMessage() {
const input = document.getElementById('message-input');
const text = input.value.trim();
if (!text || !currentPartner) return;
// Блокируем кнопку на время отправки
const sendBtn = document.getElementById('send-btn');
sendBtn.disabled = true;
input.disabled = true;
try {
const res = await fetch(`${API_PREFIX}/api/messages`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
from_id: currentUser.user_id,
to_id: currentPartner.id,
text: text
})
});
if (res.ok) {
input.value = '';
// Принудительно обновляем сообщения
await loadMessages(true);
}
} catch (err) {
console.error('Error sending message:', err);
} finally {
sendBtn.disabled = false;
input.disabled = false;
input.focus();
}
}
// Умный поллинг
function startPolling() {
if (messagesInterval) {
clearInterval(messagesInterval);
}
// Проверяем новые сообщения каждые 2 секунды
messagesInterval = setInterval(() => {
if (currentPartner) {
loadMessages(false);
}
}, 2000);
// Также проверяем список пользователей раз в 30 секунд
setInterval(() => {
if (currentUser) {
loadUsers();
}
}, 30000);
}
// Показать боковую панель на мобильных
function showSidebar() {
if (window.innerWidth <= 768) {
document.getElementById('sidebar').classList.remove('hidden');
}
}
// Enter для отправки
document.addEventListener('DOMContentLoaded', () => {
const messageInput = document.getElementById('message-input');
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Фикс для мобильных устройств - поднятие поля ввода при фокусе
messageInput.addEventListener('focus', () => {
setTimeout(() => {
const messagesDiv = document.getElementById('messages');
messagesDiv.scrollTop = messagesDiv.scrollHeight;
messageInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 300);
});
messageInput.addEventListener('blur', () => {
window.scrollTo(0, 0);
});
});
// Обработка изменения размера окна
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
if (window.innerWidth > 768) {
document.getElementById('sidebar').classList.remove('hidden');
} else if (!currentPartner) {
document.getElementById('sidebar').classList.remove('hidden');
}
}, 250);
});
// Админ панель
async function showAdminPanel() {
const panel = document.getElementById('admin-panel');
panel.style.display = 'flex';
try {
const res = await fetch(`${API_PREFIX}/api/admin/users?admin_id=${currentUser.user_id}`);
const users = await res.json();
const adminList = document.getElementById('admin-users-list');
adminList.innerHTML = '<h4 style="margin-bottom: 15px;">Все зарегистрированные пользователи:</h4>';
users.forEach(user => {
const userEl = document.createElement('div');
userEl.className = 'admin-user-item';
const statusClass = user.active ? 'status-active' : 'status-inactive';
const statusText = user.active ? 'Активен' : 'Неактивен';
userEl.innerHTML = `
<div class="user-info">
<span>${escapeHtml(user.username)} ${user.id === currentUser.user_id ? '(вы)' : ''}</span>
<span class="status-badge ${statusClass}">${statusText}</span>
</div>
<div class="admin-actions">
${user.id !== currentUser.user_id ? `
<button class="toggle-btn activate" onclick="toggleUser(${user.id}, true)">✓ Активировать</button>
<button class="toggle-btn deactivate" onclick="toggleUser(${user.id}, false)">✕ Деактивировать</button>
` : ''}
</div>
`;
adminList.appendChild(userEl);
});
} catch (err) {
console.error('Error loading admin panel:', err);
}
}
function closeAdminPanel() {
document.getElementById('admin-panel').style.display = 'none';
}
async function toggleUser(userId, active) {
try {
await fetch(`${API_PREFIX}/api/admin/toggle`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
admin_id: currentUser.user_id,
user_id: userId,
active: active
})
});
// Обновляем админ-панель и список пользователей
await showAdminPanel();
await loadUsers();
} catch (err) {
console.error('Error toggling user:', err);
}
}