View file messages/chat.php

File size: 9.28Kb
<?php
/**
 * CMS: LaiCMS (v1.0 Edition 2026)
 * File: messages/chat.php
 * Оптимизация: PHP 7.4+, Mobile First, Haptic Feedback.
 */

declare(strict_types=1);

require_once '../system/db.php';
require_once '../system/functions.php';

// 1. Проверка авторизации через null-coalescing
$my_id = (int)($_SESSION['user_id'] ?? 0);
if (!$my_id) { 
    header("Location: /users/login.php"); 
    exit; 
}

// 2. Валидация ID собеседника
$opp_id = (int)($_GET['id'] ?? 0);
if ($opp_id <= 0) {
    die("Пользователь не найден");
}

// 3. Получение данных собеседника через подготовленный запрос (Безопасность)
$stmt = $mysqli->prepare("SELECT username, avatar, last_seen, role FROM users WHERE id = ? LIMIT 1");
$stmt->bind_param("i", $opp_id);
$stmt->execute();
$opp = $stmt->get_result()->fetch_assoc();

if (!$opp) {
    die("Собеседник не существует");
}

// 4. Статус онлайн и заголовок
$is_online = (strtotime($opp['last_seen'] ?? '') > (time() - 300));
$page_title = "Чат с " . htmlspecialchars($opp['username']);

include '../system/header.php';
?>

<style>
    :root {
        --chat-bg: #fdfdfd;
        --my-bubble: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
        --opp-bubble: var(--pico-card-background-color);
    }

    [data-theme="dark"] :root { --chat-bg: #0b0e14; }

    /* Основной контейнер чата */
    .chat-wrapper {
        max-width: 1000px;
        margin: 0 auto;
        height: 85vh;
        display: flex;
        flex-direction: column;
        background: var(--chat-bg);
        border-radius: 24px;
        border: 1px solid var(--pico-muted-border-color);
        box-shadow: 0 20px 60px rgba(0,0,0,0.1);
        overflow: hidden;
        animation: chatFadeIn 0.5s ease;
    }

    /* Адаптация под мобильные устройства */
    @media (max-width: 768px) {
        .chat-wrapper {
            height: 92vh; /* Больше места на экране телефона */
            border-radius: 0;
            border: none;
        }
        #msgInput {
            font-size: 16px !important; /* Убирает авто-зум в iOS */
        }
        .send-btn {
            width: 48px !important;
            height: 48px !important;
        }
    }

    /* Шапка */
    .chat-header {
        padding: 1rem 1.5rem;
        background: var(--pico-card-sectioning-background-color);
        border-bottom: 1px solid var(--pico-muted-border-color);
        display: flex;
        justify-content: space-between;
        align-items: center;
        backdrop-filter: blur(15px);
        z-index: 10;
    }

    .user-info { display: flex; align-items: center; gap: 12px; }
    .chat-avatar { width: 45px; height: 45px; border-radius: 14px; object-fit: cover; }
    
    .status-badge { font-size: 0.75rem; display: flex; align-items: center; gap: 6px; }
    .status-dot { width: 8px; height: 8px; border-radius: 50%; }
    .online { background: #2ecc71; box-shadow: 0 0 10px rgba(46, 204, 113, 0.5); }
    .offline { background: #95a5a6; }

    /* Область сообщений */
    .chat-body {
        flex: 1;
        overflow-y: auto;
        padding: 1.5rem;
        display: flex;
        flex-direction: column;
        gap: 12px;
        background-image: radial-gradient(var(--pico-muted-border-color) 0.8px, transparent 0.8px);
        background-size: 24px 24px;
        scroll-behavior: smooth;
    }

    /* Пузыри сообщений */
    .bubble {
        max-width: 80%;
        padding: 12px 16px;
        border-radius: 20px;
        font-size: 0.95rem;
        position: relative;
        line-height: 1.4;
    }

    .bubble.me {
        align-self: flex-end;
        background: var(--my-bubble);
        color: #fff;
        border-bottom-right-radius: 4px;
    }

    .bubble.opp {
        align-self: flex-start;
        background: var(--opp-bubble);
        border: 1px solid var(--pico-muted-border-color);
        border-bottom-left-radius: 4px;
    }

    /* Нижняя панель ввода */
    .chat-footer {
        padding: 1rem 1.5rem;
        background: var(--pico-card-sectioning-background-color);
        border-top: 1px solid var(--pico-muted-border-color);
    }

    #chatForm {
        display: flex;
        gap: 10px;
        align-items: center;
        background: var(--pico-background-color);
        padding: 5px 10px;
        border-radius: 30px;
        border: 1px solid var(--pico-muted-border-color);
    }

    #msgInput { border: none !important; margin: 0 !important; flex: 1; background: transparent !important; }

    .action-btn { background: none; border: none; color: var(--pico-primary); cursor: pointer; padding: 8px; }

    .send-btn {
        background: var(--pico-primary) !important;
        color: white !important;
        width: 40px; height: 40px; border-radius: 50%;
        display: flex; align-items: center; justify-content: center;
        transition: 0.2s;
    }

    @keyframes chatFadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
</style>

<div class="chat-wrapper">
    <div class="chat-header">
        <div class="user-info">
            <a href="/users/account.php?id=<?= $opp_id ?>">
                <?php if(!empty($opp['avatar'])): ?>
                    <img src="<?= htmlspecialchars($opp['avatar']) ?>" class="chat-avatar">
                <?php else: ?>
                    <div class="chat-avatar" style="background: var(--pico-primary); color:white; display:flex; align-items:center; justify-content:center;">
                        <?= mb_substr($opp['username'], 0, 1) ?>
                    </div>
                <?php endif; ?>
            </a>
            <div>
                <strong style="display:block;"><?= htmlspecialchars($opp['username']) ?></strong>
                <span class="status-badge">
                    <div class="status-dot <?= $is_online ? 'online' : 'offline' ?>"></div>
                    <small><?= $is_online ? 'в сети' : 'был ' . date('H:i', strtotime($opp['last_seen'])) ?></small>
                </span>
            </div>
        </div>
        <button class="action-btn" onclick="location.reload()"><i class="fa-solid fa-rotate"></i></button>
    </div>

    <div class="chat-body" id="chatBox"></div>

    <div class="chat-footer">
        <form id="chatForm">
            <input type="hidden" name="opp_id" value="<?= $opp_id ?>">
            
            <label class="action-btn" style="margin:0;">
                <i class="fa-solid fa-paperclip"></i>
                <input type="file" name="attachment" id="file-input" hidden>
            </label>

            <input type="text" name="message" id="msgInput" placeholder="Сообщение..." autocomplete="off">
            
            <button type="submit" class="send-btn">
                <i class="fa-solid fa-paper-plane"></i>
            </button>
        </form>
        <div id="file-preview" style="font-size:0.7rem; color:var(--pico-primary); margin-top:5px; margin-left:15px;"></div>
    </div>
</div>

<script>
const chatBox = document.getElementById('chatBox');
const chatForm = document.getElementById('chatForm');
const msgInput = document.getElementById('msgInput');
const fileInput = document.getElementById('file-input');

// 1. Плавная прокрутка вниз
const scrollToBottom = (force = false) => {
    const isAtBottom = chatBox.scrollHeight - chatBox.scrollTop <= chatBox.clientHeight + 150;
    if (force || isAtBottom) {
        chatBox.scrollTo({ top: chatBox.scrollHeight, behavior: 'smooth' });
    }
};

// 2. Загрузка сообщений через AJAX
async function fetchMessages() {
    try {
        const res = await fetch(`get_messages_ajax.php?opp_id=<?= $opp_id ?>`);
        const html = await res.text();
        if (chatBox.innerHTML !== html) {
            chatBox.innerHTML = html;
            scrollToBottom();
        }
    } catch (e) { console.error("Update error"); }
}

// 3. Удобная отправка (телефон + ПК)
chatForm.onsubmit = async (e) => {
    e.preventDefault();
    if (!msgInput.value.trim() && !fileInput.files.length) return;

    const formData = new FormData(chatForm);
    msgInput.value = ''; // Мгновенная очистка
    document.getElementById('file-preview').innerText = '';

    // Тактильный отклик (для мобилок)
    if (window.navigator.vibrate) window.navigator.vibrate(10);

    await fetch('send_ajax.php', { method: 'POST', body: formData });
    fetchMessages();
    setTimeout(() => scrollToBottom(true), 50);
};

// 4. Отправка по нажатию Enter на клавиатуре телефона/ПК
msgInput.onkeydown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        chatForm.requestSubmit();
    }
};

// 5. Превью выбранного файла
fileInput.onchange = () => {
    if (fileInput.files[0]) {
        document.getElementById('file-preview').innerText = "📎 " + fileInput.files[0].name;
    }
};

// Интервалы и инициализация
setInterval(fetchMessages, 3000);
fetchMessages();
setTimeout(() => scrollToBottom(true), 500);
</script>

<?php include '../system/footer.php'; ?>