453 lines
23 KiB
PHP
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">×</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>
|