2025-08-27 03:14:15 +03:00
|
|
|
// Socket.io connection - for remote server
|
2025-08-27 01:48:09 +03:00
|
|
|
const socket = io(window.location.origin);
|
2024-01-04 20:11:35 +05:30
|
|
|
|
|
|
|
|
// Elements
|
|
|
|
|
const $messageForm = document.querySelector("#message-form");
|
|
|
|
|
const $messageFormInput = $messageForm.querySelector("input");
|
2025-08-27 03:14:15 +03:00
|
|
|
const $messageFormButton = $messageForm.querySelector("#send-btn");
|
2024-01-04 20:11:35 +05:30
|
|
|
const $messages = document.querySelector("#messages");
|
2025-08-27 03:14:15 +03:00
|
|
|
const $roomName = document.querySelector("#room-name");
|
|
|
|
|
const $connectionStatus = document.querySelector(".status-indicator");
|
|
|
|
|
const $statusText = document.querySelector(".status-text");
|
2024-01-04 20:11:35 +05:30
|
|
|
|
|
|
|
|
// Templates
|
|
|
|
|
const messageTemplate = document.querySelector("#message-template").innerHTML;
|
|
|
|
|
const sidebarTemplate = document.querySelector("#sidebar-template").innerHTML;
|
|
|
|
|
|
|
|
|
|
// Options
|
|
|
|
|
const { username, room } = Qs.parse(location.search, {
|
|
|
|
|
ignoreQueryPrefix: true,
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-27 03:14:15 +03:00
|
|
|
// Connection status management
|
|
|
|
|
let isConnected = true;
|
|
|
|
|
|
|
|
|
|
socket.on('connect', () => {
|
|
|
|
|
isConnected = true;
|
|
|
|
|
updateConnectionStatus(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('disconnect', () => {
|
|
|
|
|
isConnected = false;
|
|
|
|
|
updateConnectionStatus(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function updateConnectionStatus(connected) {
|
|
|
|
|
if (connected) {
|
|
|
|
|
$connectionStatus.classList.remove('offline');
|
|
|
|
|
$connectionStatus.classList.add('online');
|
|
|
|
|
$statusText.textContent = 'Connected';
|
|
|
|
|
} else {
|
|
|
|
|
$connectionStatus.classList.remove('online');
|
|
|
|
|
$connectionStatus.classList.add('offline');
|
|
|
|
|
$statusText.textContent = 'Disconnected';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auto scroll functionality
|
2024-01-04 20:11:35 +05:30
|
|
|
const autoScroll = () => {
|
|
|
|
|
// New message element
|
|
|
|
|
const $newMessage = $messages.lastElementChild;
|
|
|
|
|
|
2025-08-27 03:14:15 +03:00
|
|
|
if (!$newMessage) return;
|
|
|
|
|
|
|
|
|
|
// height of the new message
|
2024-01-04 20:11:35 +05:30
|
|
|
const newMessageStyle = getComputedStyle($newMessage);
|
|
|
|
|
const newMessageMargin = parseInt(newMessageStyle.marginBottom);
|
|
|
|
|
const newMessageHeight = $newMessage.offsetHeight + newMessageMargin;
|
|
|
|
|
|
|
|
|
|
// visible height
|
|
|
|
|
const visibleHeight = $messages.offsetHeight;
|
|
|
|
|
|
|
|
|
|
// height of messages container
|
|
|
|
|
const containerHeight = $messages.scrollHeight;
|
|
|
|
|
|
|
|
|
|
// how far have I scrolled?
|
|
|
|
|
const scrollOffset = $messages.scrollTop + visibleHeight;
|
|
|
|
|
|
|
|
|
|
if (containerHeight - newMessageHeight <= scrollOffset) {
|
|
|
|
|
$messages.scrollTop = $messages.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-27 03:14:15 +03:00
|
|
|
// Message handling
|
2024-01-04 20:11:35 +05:30
|
|
|
socket.on("message", (message) => {
|
2025-08-27 03:14:15 +03:00
|
|
|
const isOwnMessage = message.username === username;
|
2024-01-04 20:11:35 +05:30
|
|
|
const html = Mustache.render(messageTemplate, {
|
|
|
|
|
username: message.username,
|
|
|
|
|
message: message.text,
|
2025-08-27 03:14:15 +03:00
|
|
|
createdAt: moment(message.createdAt).format("HH:mm"),
|
|
|
|
|
isOwn: isOwnMessage
|
2024-01-04 20:11:35 +05:30
|
|
|
});
|
2025-08-27 03:14:15 +03:00
|
|
|
|
|
|
|
|
// Remove welcome message if it exists
|
|
|
|
|
const welcomeMessage = $messages.querySelector('.welcome-message');
|
|
|
|
|
if (welcomeMessage) {
|
|
|
|
|
welcomeMessage.remove();
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-04 20:11:35 +05:30
|
|
|
$messages.insertAdjacentHTML("beforeend", html);
|
|
|
|
|
autoScroll();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on("roomData", ({ room, users }) => {
|
2025-08-27 03:14:15 +03:00
|
|
|
const usersWithCurrent = users.map(user => ({
|
|
|
|
|
...user,
|
|
|
|
|
isCurrentUser: user.username === username
|
|
|
|
|
}));
|
|
|
|
|
|
2024-01-04 20:11:35 +05:30
|
|
|
const html = Mustache.render(sidebarTemplate, {
|
|
|
|
|
room: room,
|
2025-08-27 03:14:15 +03:00
|
|
|
users: usersWithCurrent,
|
2024-01-04 20:11:35 +05:30
|
|
|
});
|
|
|
|
|
document.querySelector("#sidebar").innerHTML = html;
|
2025-08-27 03:14:15 +03:00
|
|
|
$roomName.textContent = room;
|
2024-01-04 20:11:35 +05:30
|
|
|
});
|
|
|
|
|
|
2025-08-27 03:14:15 +03:00
|
|
|
// Form submission
|
2024-01-04 20:11:35 +05:30
|
|
|
$messageForm.addEventListener("submit", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
// disable form after submit
|
|
|
|
|
$messageFormButton.setAttribute("disabled", "disabled");
|
2025-08-27 03:14:15 +03:00
|
|
|
$messageFormButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
2024-01-04 20:11:35 +05:30
|
|
|
|
2025-08-27 03:14:15 +03:00
|
|
|
const message = $messageFormInput.value.trim();
|
2024-01-04 20:11:35 +05:30
|
|
|
|
|
|
|
|
if (message === "") {
|
|
|
|
|
// enable form after submit
|
|
|
|
|
$messageFormButton.removeAttribute("disabled");
|
2025-08-27 03:14:15 +03:00
|
|
|
$messageFormButton.innerHTML = '<i class="fas fa-paper-plane"></i>';
|
2024-01-04 20:11:35 +05:30
|
|
|
return;
|
|
|
|
|
}
|
2025-08-27 03:14:15 +03:00
|
|
|
|
2024-01-04 20:11:35 +05:30
|
|
|
socket.emit("sendMessage", message, (error) => {
|
|
|
|
|
// enable form after submit
|
|
|
|
|
$messageFormButton.removeAttribute("disabled");
|
2025-08-27 03:14:15 +03:00
|
|
|
$messageFormButton.innerHTML = '<i class="fas fa-paper-plane"></i>';
|
2024-01-04 20:11:35 +05:30
|
|
|
// clear input
|
|
|
|
|
$messageFormInput.value = "";
|
|
|
|
|
// focus input
|
|
|
|
|
$messageFormInput.focus();
|
2025-08-27 03:14:15 +03:00
|
|
|
|
2024-01-04 20:11:35 +05:30
|
|
|
if (error) {
|
2025-08-27 03:14:15 +03:00
|
|
|
showNotification(error, 'error');
|
2024-01-04 20:11:35 +05:30
|
|
|
return console.log(error);
|
|
|
|
|
}
|
2025-08-27 03:14:15 +03:00
|
|
|
|
|
|
|
|
showNotification('Message sent!', 'success');
|
2024-01-04 20:11:35 +05:30
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-27 03:14:15 +03:00
|
|
|
// Join room
|
|
|
|
|
socket.emit("join", { username, room }, (error) => {
|
|
|
|
|
if (error) {
|
|
|
|
|
showNotification(error, 'error');
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
location.href = "/";
|
|
|
|
|
}, 2000);
|
2024-01-04 20:11:35 +05:30
|
|
|
}
|
2025-08-27 03:14:15 +03:00
|
|
|
});
|
2024-01-04 20:11:35 +05:30
|
|
|
|
2025-08-27 03:14:15 +03:00
|
|
|
// Notification system
|
|
|
|
|
function showNotification(message, type = 'info') {
|
|
|
|
|
// Remove existing notifications
|
|
|
|
|
const existingNotifications = document.querySelectorAll('.notification');
|
|
|
|
|
existingNotifications.forEach(notification => notification.remove());
|
|
|
|
|
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
notification.className = `notification notification-${type}`;
|
|
|
|
|
notification.innerHTML = `
|
|
|
|
|
<i class="fas fa-${getNotificationIcon(type)}"></i>
|
|
|
|
|
<span>${message}</span>
|
|
|
|
|
<button onclick="this.parentElement.remove()">
|
|
|
|
|
<i class="fas fa-times"></i>
|
|
|
|
|
</button>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
|
|
|
|
|
|
// Auto remove after 5 seconds
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (notification.parentElement) {
|
|
|
|
|
notification.remove();
|
2024-01-04 20:11:35 +05:30
|
|
|
}
|
2025-08-27 03:14:15 +03:00
|
|
|
}, 5000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getNotificationIcon(type) {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'success': return 'check-circle';
|
|
|
|
|
case 'error': return 'exclamation-circle';
|
|
|
|
|
case 'warning': return 'exclamation-triangle';
|
|
|
|
|
default: return 'info-circle';
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-04 20:11:35 +05:30
|
|
|
|
2025-08-27 03:14:15 +03:00
|
|
|
// Keyboard shortcuts
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
|
// Ctrl/Cmd + Enter to send message
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
|
|
|
$messageForm.dispatchEvent(new Event('submit'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Escape to clear input
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
$messageFormInput.value = '';
|
|
|
|
|
$messageFormInput.blur();
|
|
|
|
|
}
|
2024-01-04 20:11:35 +05:30
|
|
|
});
|
|
|
|
|
|
2025-08-27 03:14:15 +03:00
|
|
|
// Input focus management
|
|
|
|
|
$messageFormInput.addEventListener('focus', () => {
|
|
|
|
|
$messageFormInput.parentElement.classList.add('focused');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$messageFormInput.addEventListener('blur', () => {
|
|
|
|
|
if (!$messageFormInput.value) {
|
|
|
|
|
$messageFormInput.parentElement.classList.remove('focused');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Page visibility API for better UX
|
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
|
|
|
if (document.hidden) {
|
|
|
|
|
document.title = '💬 Chat App - Background';
|
|
|
|
|
} else {
|
|
|
|
|
document.title = 'Chat App - Chat';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Prevent form submission on Enter if Shift is held (for multi-line support)
|
|
|
|
|
$messageFormInput.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Enter' && e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
// Insert new line
|
|
|
|
|
const start = e.target.selectionStart;
|
|
|
|
|
const end = e.target.selectionEnd;
|
|
|
|
|
const value = e.target.value;
|
|
|
|
|
e.target.value = value.substring(0, start) + '\n' + value.substring(end);
|
|
|
|
|
e.target.selectionStart = e.target.selectionEnd = start + 1;
|
2024-01-04 20:11:35 +05:30
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|