// Chat System Module // Handles chat functionality, message handling, and user authentication (function() { 'use strict'; AppModules.require('api', 'ui-controls', 'screen-reader'); AppModules.register('chat'); // Initialize chat system function initializeChat() { AppLogger.log('Initializing chat system...'); // Get user ID first API.getUserId() .then(data => { AppLogger.log('Chat initialized with user:', AppState.userId, AppState.isAdmin ? '(admin)' : ''); // Show admin toast if applicable if (AppState.isAdmin) { UIControls.showToast('Admin mode activated'); } // Announce chat initialization to screen readers ScreenReader.connectionStatus('connected'); // Set up nickname handling const nicknameInput = document.getElementById('nickname'); if (nicknameInput) { // Load saved nickname const savedNickname = localStorage.getItem('chatNickname'); if (savedNickname) { nicknameInput.value = savedNickname; } // Handle nickname changes UIControls.DOMUtils.addEvent(nicknameInput, 'change', function() { const nickname = this.value.trim(); localStorage.setItem('chatNickname', nickname); AppState.nickname = nickname; }); } // Set up message input handling const messageInput = document.getElementById('messageInput'); if (messageInput) { // Enter key to send message UIControls.DOMUtils.addEvent(messageInput, 'keypress', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Typing indicator UIControls.DOMUtils.addEvent(messageInput, 'input', function() { handleTypingIndicator(); }); } // Set up send button const sendButton = document.querySelector('.chat-input button') || document.querySelector('#messageInput').nextElementSibling; if (sendButton && sendButton.tagName === 'BUTTON') { UIControls.DOMUtils.addEvent(sendButton, 'click', sendMessage); } // Start polling for messages startMessagePolling(); // Send initial heartbeat API.sendHeartbeat(); AppLogger.log('Chat system initialized successfully'); }) .catch(error => { AppLogger.error('Failed to initialize chat:', error); UIControls.showToast('Failed to initialize chat'); }); } // Validation helper functions function validateNickname(nickname) { if (!nickname) { return { valid: false, message: 'Nickname is required' }; } const trimmed = nickname.trim(); if (trimmed.length === 0) { return { valid: false, message: 'Nickname cannot be empty' }; } if (trimmed.length > 20) { return { valid: false, message: 'Nickname must be 20 characters or less' }; } if (trimmed.length < 1) { return { valid: false, message: 'Nickname must be at least 1 character' }; } // Allow alphanumeric characters and spaces, apostrophes, hyphens const nicknameRegex = /^[a-zA-Z0-9\s'-]+$/; if (!nicknameRegex.test(trimmed)) { return { valid: false, message: 'Nickname can only contain letters, numbers, spaces, hyphens, and apostrophes' }; } return { valid: true }; } function validateMessage(message) { if (!message) { return { valid: false, message: 'Message is required' }; } const trimmed = message.trim(); if (trimmed.length === 0) { return { valid: false, message: 'Message cannot be empty' }; } if (trimmed.length > 1000) { return { valid: false, message: 'Message must be 1000 characters or less' }; } return { valid: true }; } function updateValidationUI(field, errorElement, isValid, errorMessage) { const formGroup = errorElement.closest('.form-group'); const input = formGroup.querySelector('input'); formGroup.classList.remove('form-group--error', 'form-group--success'); if (isValid) { formGroup.classList.add('form-group--success'); errorElement.style.display = 'none'; } else { formGroup.classList.add('form-group--error'); errorElement.textContent = errorMessage; errorElement.style.display = 'block'; } } function clearValidationState(field) { const formGroup = document.querySelector(`#${field}`).closest('.form-group'); if (formGroup) { formGroup.classList.remove('form-group--error', 'form-group--success'); const errorElement = document.getElementById(`${field}Error`); if (errorElement) { errorElement.style.display = 'none'; } } } // Send message functionality function sendMessage() { const messageInput = document.getElementById('messageInput'); const nicknameInput = document.getElementById('nickname'); const sendButton = document.querySelector('.chat-input button') || document.querySelector('#messageInput').nextElementSibling; const nicknameErrorElement = document.getElementById('nicknameError'); const messageErrorElement = document.getElementById('messageError'); const message = messageInput?.value?.trim() || ''; const nickname = nicknameInput?.value?.trim() || ''; // Validate nickname const nicknameValidation = validateNickname(nickname); updateValidationUI('nickname', nicknameErrorElement, nicknameValidation.valid, nicknameValidation.message); // Announce validation errors if (!nicknameValidation.valid) { ScreenReader.formValidation('nickname', false, nicknameValidation.message); } // Validate message const messageValidation = validateMessage(message); updateValidationUI('messageInput', messageErrorElement, messageValidation.valid, messageValidation.message); // Announce validation errors if (!messageValidation.valid) { ScreenReader.formValidation('messageInput', false, messageValidation.message); } // If either validation failed, focus the appropriate field and stop if (!nicknameValidation.valid) { nicknameInput.focus(); return; } if (!messageValidation.valid) { messageInput.focus(); return; } // Show loading state on send button const loadingContext = window.LoadingStates?.showButtonLoading(sendButton, 'Sending...'); // All validation passed - send message via API API.sendMessage(nickname, message) .then(data => { if (data.success) { // Clear the form and show success if (messageInput) messageInput.value = ''; AppState.nickname = nickname; // Show success confirmation updateValidationUI('nickname', nicknameErrorElement, true); updateValidationUI('messageInput', messageErrorElement, true); // Announce successful message send ScreenReader.formSuccess('Message sent successfully'); UIControls.showToast('Message sent successfully!'); // Clear success state after a delay setTimeout(() => { clearValidationState('nickname'); clearValidationState('messageInput'); }, 3000); AppLogger.log('Message sent successfully'); } else if (data.error) { // Show server error updateValidationUI('messageInput', messageErrorElement, false, 'Failed to send message: ' + data.error); UIControls.updateConnectionStatus(false); ScreenReader.connectionStatus('error', data.error); } }) .catch(error => { AppLogger.error('Error sending message:', error); updateValidationUI('messageInput', messageErrorElement, false, 'Failed to send message. Please try again.'); UIControls.updateConnectionStatus(false); ScreenReader.connectionStatus('error', 'Network error while sending message'); }) .finally(() => { // End loading state if (loadingContext) { loadingContext.end(true); } }); } // Delete message (admin only) function deleteMessage(messageId) { if (!AppState.isAdmin) return; if (!confirm('Delete this message?')) return; API.deleteMessage(messageId) .then(data => { if (data.success) { removeMessageFromUI(messageId); UIControls.showToast('Message deleted'); ScreenReader.systemMessage('Message deleted by admin', 'normal'); } else { UIControls.showToast('Failed to delete message'); } }) .catch(error => { AppLogger.error('Error deleting message:', error); UIControls.showToast('Failed to delete message'); }); } // Ban user (admin only) function banUser(userId) { if (!AppState.isAdmin) return; if (!confirm('Ban this user from chat?')) return; API.banUser(userId) .then(data => { if (data.success) { UIControls.showToast('User banned'); ScreenReader.systemMessage('User banned from chat by admin', 'normal'); } else { UIControls.showToast('Failed to ban user'); } }) .catch(error => { AppLogger.error('Error banning user:', error); UIControls.showToast('Failed to ban user'); }); } // Clear chat (admin only) function clearChat() { if (!AppState.isAdmin) return; if (!confirm('Clear all chat messages?')) return; API.clearChat() .then(data => { if (data.success) { clearMessagesUI(); UIControls.showToast('Chat cleared'); ScreenReader.systemMessage('Chat was cleared by admin', 'high'); AppState.lastMessageId = ''; API.sendHeartbeat(); // Force refresh all users } else { UIControls.showToast('Failed to clear chat'); } }) .catch(error => { AppLogger.error('Error clearing chat:', error); UIControls.showToast('Failed to clear chat'); }); } // Message polling system function startMessagePolling() { // Poll for new messages setInterval(fetchMessages, AppConfig.api.chatPollInterval); // Send heartbeat periodically setInterval(API.sendHeartbeat, AppConfig.api.heartbeatInterval); // Initial fetch fetchMessages(); } // Fetch and display messages function fetchMessages() { API.fetchMessages() .then(data => { if (data.success) { // Handle initial load or message updates if (data.all_messages !== null) { // Initial load or forced refresh (like after admin clear) handleInitialMessageLoad(data); } else { // Incremental updates handleNewMessages(data.messages); } // Check for admin clear if (data.message_count === 0 && AppState.allMessages.length > 0) { handleChatCleared(); } // Update last message ID if (AppState.allMessages.length > 0) { AppState.lastMessageId = AppState.allMessages[AppState.allMessages.length - 1].id; } UIControls.updateConnectionStatus(true); } }) .catch(error => { AppLogger.error('Error fetching messages:', error); UIControls.updateConnectionStatus(false); }); } // Handle initial message load function handleInitialMessageLoad(data) { AppState.allMessages = data.all_messages || []; // Announce message group if loading multiple messages if (AppState.allMessages.length > 0) { ScreenReader.messageGroup(AppState.allMessages, 'added'); } displayAllMessages(); } // Handle new messages function handleNewMessages(messages) { if (!messages || messages.length === 0) return; messages.forEach(msg => { appendMessage(msg); // Show notification if chat is collapsed and message is not from current user if (AppState.chatCollapsed && msg.user_id !== AppState.userId) { AppState.unreadCount++; UIControls.updateNotificationBadge(); UIControls.playNotificationSound(); } }); } // Handle chat cleared by admin function handleChatCleared() { const chatMessages = document.getElementById('chatMessages'); if (chatMessages) { chatMessages.innerHTML = '
'; } AppState.allMessages = []; AppState.lastMessageId = ''; } // Typing indicator system function handleTypingIndicator() { const messageInput = document.getElementById('messageInput'); if (!messageInput) return; const hasContent = messageInput.value.length > 0; if (hasContent && !AppState.isTyping) { AppState.isTyping = true; } clearTimeout(AppState.typingTimer); AppState.typingTimer = setTimeout(() => { AppState.isTyping = false; }, AppConfig.chat.typingTimeout); } // UI manipulation functions function displayAllMessages() { const chatMessages = document.getElementById('chatMessages'); if (!chatMessages) return; if (AppState.allMessages.length === 0) { chatMessages.innerHTML = '