iptv-stream-web/assets/js/chat.js

544 lines
20 KiB
JavaScript

// 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 = '<div class="system-message">Chat was cleared by admin</div>';
}
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 = '<div class="empty-chat">No messages yet. Be the first to say hello! 👋</div>';
AppState.lastMessageCount = 0;
return;
}
chatMessages.innerHTML = '';
AppState.allMessages.forEach(msg => {
appendMessageElement(msg, false);
});
// Scroll to bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
AppState.lastMessageCount = AppState.allMessages.length;
}
function appendMessage(msg) {
AppState.allMessages.push(msg);
appendMessageElement(msg, true);
AppState.lastMessageCount = AppState.allMessages.length;
}
function appendMessageElement(msg, animate) {
const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return;
// Remove empty chat message if it exists
const emptyChat = chatMessages.querySelector('.empty-chat');
if (emptyChat) {
emptyChat.remove();
}
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
messageDiv.setAttribute('data-message-id', msg.id);
// Own message styling
if (msg.user_id === AppState.userId) {
messageDiv.className += ' own-message';
}
// Admin message styling
if (msg.is_admin) {
messageDiv.className += ' admin-message';
}
// Disable animation for initial load
if (!animate) {
messageDiv.style.animation = 'none';
}
// Create admin actions if user is admin
const adminActions = (AppState.isAdmin && msg.user_id !== AppState.userId) ?
`<div class="message-actions">
<button class="delete-btn" onclick="Chat.deleteMessage('${msg.id}')">Delete</button>
<button class="ban-btn" onclick="Chat.banUser('${msg.user_id}')">Ban</button>
</div>` : '';
// Create admin badge
const adminBadge = msg.is_admin ? '👑 ' : '';
messageDiv.innerHTML = `
<div class="message-header">
<span class="message-nickname${msg.is_admin ? ' admin' : ''}">${adminBadge}${msg.nickname}</span>
<span class="message-id">#${msg.user_id}</span>
<span class="message-time">${msg.time}</span>
</div>
<div class="message-text">${escapeHtml(msg.message)}</div>
${adminActions}
`;
chatMessages.appendChild(messageDiv);
// Auto-scroll to bottom on new messages
if (animate) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
function removeMessageFromUI(messageId) {
const messageElement = document.querySelector(`[data-message-id="${messageId}"]`);
if (messageElement) {
messageElement.style.animation = 'slideOut 0.3s';
setTimeout(() => {
messageElement.remove();
}, 300);
}
// Remove from internal array
AppState.allMessages = AppState.allMessages.filter(msg => msg.id !== messageId);
}
function clearMessagesUI() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.innerHTML = '<div class="system-message">Chat cleared by admin</div>';
}
AppState.allMessages = [];
AppState.lastMessageId = '';
}
// Utility function for HTML escaping
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Public API
window.Chat = {
initializeChat: initializeChat,
sendMessage: sendMessage,
deleteMessage: deleteMessage,
banUser: banUser,
clearChat: clearChat,
displayAllMessages: displayAllMessages,
appendMessage: appendMessage
};
// Initialize when DOM is ready and modules are loaded
document.addEventListener('DOMContentLoaded', function() {
// Small delay to ensure all modules are loaded
setTimeout(initializeChat, 100);
});
AppLogger.log('Chat module loaded');
})();