36163-vm/index.php
2025-11-24 05:17:23 +00:00

453 lines
23 KiB
PHP

<?php
session_start();
require_once 'db/config.php';
// If the user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
$current_user_id = $_SESSION['user_id'];
$current_username = $_SESSION['username'];
$pdo = db();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Fetch all users for the contact list
$stmt = $pdo->prepare("SELECT id, username FROM users WHERE id != ? ORDER BY username ASC");
$stmt->execute([$current_user_id]);
$users = $stmt->fetchAll();
// Fetch all rooms the user is a member of
$stmt = $pdo->prepare(
"SELECT
r.id,
r.name,
r.is_private,
(SELECT u.username FROM users u JOIN room_members rm_other ON u.id = rm_other.user_id WHERE rm_other.room_id = r.id AND rm_other.user_id != ?) AS private_chat_partner
FROM rooms r
JOIN room_members rm ON r.id = rm.room_id
WHERE rm.user_id = ?
ORDER BY r.is_private, r.name ASC"
);
$stmt->execute([$current_user_id, $current_user_id]);
$rooms = $stmt->fetchAll();
// If user has no rooms, create a "General" one and add them to it
if (empty($rooms)) {
$stmt = $pdo->prepare("INSERT INTO rooms (name, created_by) VALUES ('General', ?)");
$stmt->execute([$current_user_id]);
$general_room_id = $pdo->lastInsertId();
$stmt = $pdo->prepare("INSERT INTO room_members (room_id, user_id) VALUES (?, ?)");
$stmt->execute([$general_room_id, $current_user_id]);
// Re-fetch rooms
$stmt->execute([$current_user_id, $current_user_id]);
$rooms = $stmt->fetchAll();
}
// Determine the current room
$current_room_id = $_GET['room_id'] ?? $rooms[0]['id'] ?? null;
$current_room = null;
if ($current_room_id) {
foreach ($rooms as $room) {
if ($room['id'] == $current_room_id) {
$current_room = $room;
break;
}
}
}
// If the user is not a member of the requested room, redirect to their first room
if ($current_room_id && !$current_room) {
header("Location: index.php");
exit;
}
// Messages are now fetched by the frontend
$messages = [];
$last_message_id = 0;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>AD Messaging App</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet"/>
<script>
tailwind.config = {
darkMode: "class",
theme: { extend: { colors: { "primary": "#005A9C", "primary-light": "#e6f3ff", "background-light": "#f4f7fa", "background-dark": "#101922", "text-light": "#212529", "text-dark": "#f4f7fa", "border-light": "#dee2e6", "border-dark": "#324d67", "placeholder-light": "#6c757d", "placeholder-dark": "#92adc9", "input-bg-light": "#ffffff", "input-bg-dark": "#192633", "accent-blue-hover": "#007bff", "success": "#28a745", "warning": "#ffc107", "danger": "#dc3545", "info": "#17a2b8", "sidebar-bg": "#ffffff", "sidebar-bg-dark": "#0f1419" }, fontFamily: { "display": ["Inter", "sans-serif"] } } }
}
</script>
<style>
::-webkit-scrollbar { width: 6px; }
.dark ::-webkit-scrollbar-track { background: #192633; }
.dark ::-webkit-scrollbar-thumb { background: #324d67; }
html:not(.dark) ::-webkit-scrollbar-thumb { background: #ced4da; }
[x-cloak] { display: none !important; }
.tab-content { display: none; }
.tab-content.active { display: block; }
</style>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="bg-background-light dark:bg-background-dark font-display text-text-light dark:text-text-dark">
<div class="flex h-screen text-sm">
<!-- Sidebar -->
<div class="w-80 flex-shrink-0 bg-white dark:bg-background-dark border-r border-border-light dark:border-border-dark flex flex-col">
<div class="p-4 border-b border-border-light dark:border-border-dark flex justify-between items-center">
<h2 class="text-xl font-bold">Messages</h2>
<div x-data="themeSwitcher()" class="relative">
<button @click="open = !open" class="p-2 text-gray-500 rounded-lg dark:text-gray-400 hover:bg-primary-light dark:hover:bg-input-bg-dark hover:text-primary dark:hover:text-white" title="Settings"><span class="material-symbols-outlined">settings</span></button>
<div x-show="open" @click.away="open = false" x-cloak class="absolute top-12 right-0 w-48 bg-white dark:bg-input-bg-dark rounded-lg shadow-lg border dark:border-border-dark py-1 z-10">
<p class="px-4 py-2 text-xs text-gray-400">Theme</p>
<button @click="setTheme('light')" class="w-full flex items-center px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700"><span class="material-symbols-outlined mr-2">light_mode</span> Light</button>
<button @click="setTheme('dark')" class="w-full flex items-center px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700"><span class="material-symbols-outlined mr-2">dark_mode</span> Dark</button>
<button @click="setTheme('system')" class="w-full flex items-center px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700"><span class="material-symbols-outlined mr-2">desktop_windows</span> System</button>
</div>
</div>
</div>
<div x-data="{ tab: 'rooms' }" class="flex-1 flex flex-col">
<div class="border-b border-border-light dark:border-border-dark">
<nav class="flex -mb-px">
<button @click="tab = 'rooms'" :class="{ 'border-primary text-primary': tab === 'rooms', 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300': tab !== 'rooms' }" class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm focus:outline-none">Rooms</button>
<button @click="tab = 'users'" :class="{ 'border-primary text-primary': tab === 'users', 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300': tab !== 'users' }" class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm focus:outline-none">Users</button>
</nav>
</div>
<div class="flex-1 overflow-y-auto">
<!-- Rooms Tab -->
<div x-show="tab === 'rooms'" class="tab-content active">
<?php foreach ($rooms as $room):
$room_name = $room['is_private'] ? $room['private_chat_partner'] : $room['name'];
$avatar_text = $room['is_private'] ? strtoupper(substr($room['private_chat_partner'], 0, 1)) : strtoupper(substr($room['name'], 0, 1));
?>
<a href="?room_id=<?= $room['id'] ?>" class="flex items-center p-4 border-b border-border-light dark:border-border-dark <?= ($current_room && $current_room['id'] == $room['id']) ? 'bg-primary-light dark:bg-input-bg-dark' : 'hover:bg-gray-50 dark:hover:bg-gray-800' ?>">
<div class="w-12 h-12 rounded-full mr-4 bg-primary text-white flex items-center justify-center font-bold text-xl">
<?= htmlspecialchars($avatar_text) ?>
</div>
<div class="flex-1">
<h3 class="font-bold text-text-light dark:text-text-dark"><?= htmlspecialchars($room_name) ?></h3>
</div>
</a>
<?php endforeach; ?>
</div>
<!-- Users Tab -->
<div x-show="tab === 'users'" class="tab-content">
<?php foreach ($users as $user):
?>
<a href="#" @click.prevent="startPrivateChat(<?= $user['id'] ?>)" class="flex items-center p-4 border-b border-border-light dark:border-border-dark hover:bg-gray-50 dark:hover:bg-gray-800">
<img src="https://i.pravatar.cc/40?u=<?= htmlspecialchars($user['username']) ?>" alt="User Avatar" class="w-12 h-12 rounded-full mr-4">
<div class="flex-1">
<h3 class="font-bold text-text-light dark:text-text-dark"><?= htmlspecialchars($user['username']) ?></h3>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<div x-show="tab === 'rooms'" class="p-4 border-t border-border-light dark:border-border-dark">
<form id="create-room-form">
<input type="text" name="room_name" placeholder="Create new room..." class="w-full px-4 py-2 rounded-lg bg-input-bg-light dark:bg-input-bg-dark border focus:ring-2 focus:ring-primary outline-none" required>
</form>
</div>
</div>
<div class="p-4 border-t border-border-light dark:border-border-dark flex items-center justify-between">
<div class="flex items-center">
<img src="https://i.pravatar.cc/40?u=<?= htmlspecialchars($current_username) ?>" alt="My Avatar" class="w-10 h-10 rounded-full mr-3">
<span class="font-bold"><?= htmlspecialchars($current_username) ?></span>
</div>
<a href="logout.php" class="p-2 text-gray-500 rounded-lg dark:text-gray-400 hover:bg-primary-light dark:hover:bg-input-bg-dark hover:text-primary dark:hover:text-white" title="Logout"><span class="material-symbols-outlined">logout</span></a>
</div>
</div>
<!-- Main Chat Area -->
<div class="flex-1 flex flex-col">
<?php if ($current_room):
$header_name = $current_room['is_private'] ? $current_room['private_chat_partner'] : $current_room['name'];
?>
<div class="flex items-center justify-between p-4 border-b bg-white dark:bg-background-dark border-border-light dark:border-border-dark">
<div>
<h3 class="text-lg font-bold"><?= htmlspecialchars($header_name) ?></h3>
</div>
</div>
<div id="messages-container" class="flex-1 p-6 overflow-y-auto space-y-6">
<div id="no-messages" class="text-center text-gray-500">Loading messages...</div>
</div>
<div class="p-4 bg-white dark:bg-background-dark border-t border-border-light dark:border-border-dark">
<form id="send-message-form" class="relative" enctype="multipart/form-data">
<input type="hidden" name="room_id" value="<?= $current_room['id'] ?>">
<input type="text" name="message" placeholder="Type a message..." class="w-full pr-24 pl-12 py-3 rounded-full bg-input-bg-light dark:bg-input-bg-dark border focus:ring-2 focus:ring-primary outline-none" autocomplete="off">
<div class="absolute left-4 top-1/2 -translate-y-1/2 flex items-center space-x-3">
<label for="attachment-input" class="cursor-pointer text-gray-500 dark:text-placeholder-dark hover:text-primary">
<span class="material-symbols-outlined">attach_file</span>
</label>
<input type="file" name="attachment" id="attachment-input" class="hidden">
</div>
<button type="submit" class="absolute right-3 top-1/2 -translate-y-1/2 bg-primary text-white rounded-full p-2 hover:bg-accent-blue-hover"><span class="material-symbols-outlined">send</span></button>
</form>
<div id="attachment-preview" class="mt-2"></div>
</div>
<?php else:
?>
<div class="flex-1 flex items-center justify-center text-gray-500">
<p>Create or select a room to start chatting.</p>
</div>
<?php endif; ?>
</div>
</div>
<script>
let lastMessageId = 0;
const currentRoomId = <?= $current_room_id ?? 'null' ?>;
const currentUsername = "<?= htmlspecialchars($current_username) ?>";
function themeSwitcher() {
return {
open: false,
theme: 'system',
init() {
this.theme = localStorage.getItem('theme') || 'system';
this.applyTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (this.theme === 'system') this.applySystemTheme();
});
},
applyTheme() {
if (this.theme === 'dark') {
document.documentElement.classList.add('dark');
} else if (this.theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
this.applySystemTheme();
}
},
applySystemTheme() {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
},
setTheme(newTheme) {
this.theme = newTheme;
localStorage.setItem('theme', newTheme);
this.applyTheme();
this.open = false;
}
}
}
function scrollToBottom() {
const messagesContainer = document.getElementById('messages-container');
if(messagesContainer) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
scrollToBottom();
async function startPrivateChat(userId) {
const formData = new FormData();
formData.append('action', 'start_private_chat');
formData.append('user_id', userId);
try {
const response = await fetch('api.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
window.location.href = `index.php?room_id=${result.room_id}`;
} else {
alert('Error starting chat: ' + result.error);
}
} catch (error) {
console.error('Failed to start private chat:', error);
alert('An unexpected error occurred.');
}
}
function renderMessage(msg) {
const messagesContainer = document.getElementById('messages-container');
const noMessagesEl = document.getElementById('no-messages');
if (noMessagesEl) noMessagesEl.style.display = 'none';
const isSender = msg.username === currentUsername;
let attachmentHtml = '';
if (msg.file_name) {
attachmentHtml = `
<div class="mt-2">
<a href="${msg.file_path}" target="_blank" class="text-blue-500 hover:underline">${escapeHTML(msg.file_name)}</a>
<span class="text-xs text-gray-500">(${(msg.file_size / 1024 / 1024).toFixed(2)} MB)</span>
</div>
`;
}
const messageHtml = `
<div class="flex items-start gap-3 ${isSender ? 'justify-end' : ''}">
${!isSender ? `<img src="https://i.pravatar.cc/40?u=${escapeHTML(msg.username)}" alt="Avatar" class="w-10 h-10 rounded-full">` : ''}
<div class="${isSender ? 'bg-primary text-white' : 'bg-white dark:bg-input-bg-dark'} p-3 rounded-lg max-w-lg shadow">
<p class="text-sm font-bold mb-1">${escapeHTML(msg.username)}</p>
<p class="text-sm">${escapeHTML(msg.message).replace(/\n/g, '<br>')}</p>
${attachmentHtml}
<span class="text-xs ${isSender ? 'text-blue-200' : 'text-gray-400'} mt-1 block text-right">${new Date(msg.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
</div>
${isSender ? `<img src="https://i.pravatar.cc/40?u=${escapeHTML(currentUsername)}" alt="My Avatar" class="w-10 h-10 rounded-full">` : ''}
</div>
`;
messagesContainer.insertAdjacentHTML('beforeend', messageHtml);
}
function escapeHTML(str) {
if (str === null || str === undefined) {
return '';
}
var p = document.createElement("p");
p.appendChild(document.createTextNode(str));
return p.innerHTML;
}
async function pollNewMessages() {
if (!currentRoomId) return;
try {
const response = await fetch(`api.php?action=get_new_messages&room_id=${currentRoomId}&last_message_id=${lastMessageId}`);
if (response.ok) {
const messages = await response.json();
if (messages.length > 0) {
const noMessagesEl = document.getElementById('no-messages');
if(noMessagesEl && noMessagesEl.innerText === 'Loading messages...') {
noMessagesEl.style.display = 'none';
}
messages.forEach(renderMessage);
lastMessageId = messages[messages.length - 1].id;
scrollToBottom();
} else {
const noMessagesEl = document.getElementById('no-messages');
if(noMessagesEl && noMessagesEl.innerText === 'Loading messages...') {
noMessagesEl.innerText = 'No messages yet. Start the conversation!';
}
}
}
} catch (error) {
console.error('Polling error:', error);
}
// Continue polling for real-time updates
setTimeout(pollNewMessages, 1000);
}
async function fetchInitialMessages() {
if (!currentRoomId) return;
const response = await fetch(`api.php?action=get_new_messages&room_id=${currentRoomId}&last_message_id=0`);
if(response.ok) {
const messages = await response.json();
const messagesContainer = document.getElementById('messages-container');
const noMessagesEl = document.getElementById('no-messages');
if (messages.length > 0) {
noMessagesEl.style.display = 'none';
messages.forEach(renderMessage);
lastMessageId = messages[messages.length - 1].id;
scrollToBottom();
} else {
noMessagesEl.innerText = 'No messages yet. Start the conversation!';
}
}
// Start long polling after initial fetch
setTimeout(pollNewMessages, 1000);
}
document.addEventListener('DOMContentLoaded', () => {
const sendMessageForm = document.getElementById('send-message-form');
const attachmentInput = document.getElementById('attachment-input');
const attachmentPreview = document.getElementById('attachment-preview');
if(attachmentInput) {
attachmentInput.addEventListener('change', () => {
const file = attachmentInput.files[0];
if (file) {
attachmentPreview.innerHTML = `
<div class="flex items-center gap-2 text-sm">
<span>${file.name}</span>
<button type="button" id="remove-attachment" class="text-red-500">&times;</button>
</div>
`;
document.getElementById('remove-attachment').addEventListener('click', () => {
attachmentInput.value = '';
attachmentPreview.innerHTML = '';
});
}
});
}
if(sendMessageForm) {
sendMessageForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(sendMessageForm);
// Clear form fields immediately for better UX
sendMessageForm.querySelector('input[name="message"]').value = '';
if(attachmentInput) attachmentInput.value = '';
if(attachmentPreview) attachmentPreview.innerHTML = '';
try {
const response = await fetch('api.php?action=send_message', { method: 'POST', body: formData });
const result = await response.json();
if (!result.success) {
alert('Error sending message: ' + result.error);
}
// Don't manually render, let the poller pick it up
} catch (error) {
console.error('Failed to send message:', error);
alert('An unexpected error occurred.');
}
});
}
const createRoomForm = document.getElementById('create-room-form');
if(createRoomForm) {
createRoomForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(createRoomForm);
formData.append('action', 'create_room');
try {
const response = await fetch('api.php', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
window.location.href = `index.php?room_id=${result.room_id}`;
} else {
alert('Error creating room: ' + result.error);
}
} catch (error) {
console.error('Failed to create room:', error);
alert('An unexpected error occurred.');
}
});
}
if (currentRoomId) {
fetchInitialMessages();
} else {
const noMessagesEl = document.getElementById('no-messages');
if(noMessagesEl) noMessagesEl.innerText = 'Create or select a room to start chatting.';
}
});
</script>
</body>
</html>