/** * Message System JavaScript * Handles interactive features for the modern message interface */ class MessageSystem { constructor() { this.init(); } init() { this.initSearch(); this.initFolderNavigation(); this.initMessageActions(); this.initComposeFeatures(); this.initKeyboardShortcuts(); this.initAutoSave(); this.initAttachments(); this.initTooltips(); } /** * Initialize search functionality */ initSearch() { const searchInputs = document.querySelectorAll('.search-input'); searchInputs.forEach(input => { let searchTimeout; input.addEventListener('input', (e) => { clearTimeout(searchTimeout); const searchTerm = e.target.value.toLowerCase(); searchTimeout = setTimeout(() => { this.performSearch(searchTerm); }, 300); }); // Clear search on Escape key input.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.target.value = ''; this.performSearch(''); } }); }); } /** * Perform search across message items */ performSearch(searchTerm) { const messageItems = document.querySelectorAll('.message-item'); let visibleCount = 0; messageItems.forEach(item => { const text = item.textContent.toLowerCase(); if (text.includes(searchTerm)) { item.style.display = 'flex'; visibleCount++; } else { item.style.display = 'none'; } }); // Show no results message if needed this.updateSearchResults(visibleCount, searchTerm); } /** * Update search results display */ updateSearchResults(count, searchTerm) { let noResults = document.querySelector('.search-no-results'); if (count === 0 && searchTerm) { if (!noResults) { noResults = document.createElement('div'); noResults.className = 'search-no-results'; noResults.innerHTML = `

{% trans "No messages found" %}

{% trans "No messages match your search for" %} "${searchTerm}"

`; const messagesList = document.querySelector('.messages-list'); if (messagesList) { messagesList.appendChild(noResults); } } } else if (noResults) { noResults.remove(); } } /** * Initialize folder navigation */ initFolderNavigation() { const folderItems = document.querySelectorAll('.folder-item'); folderItems.forEach(item => { item.addEventListener('click', (e) => { // Remove active class from all items folderItems.forEach(f => f.classList.remove('active')); // Add active class to clicked item item.classList.add('active'); // Add loading state this.showLoadingState(); // Navigate to folder (if it's a link) if (item.tagName === 'A') { // Let the link handle navigation return; } }); }); } /** * Initialize message actions */ initMessageActions() { // Refresh button const refreshBtn = document.querySelector('.action-btn[title="Refresh"]'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.refreshMessages(); }); } // Mark as read functionality const markReadBtns = document.querySelectorAll('[onclick*="markAsRead"]'); markReadBtns.forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); this.markAsRead(btn); }); }); // Delete message functionality const deleteBtns = document.querySelectorAll('[onclick*="confirm"]'); deleteBtns.forEach(btn => { btn.addEventListener('click', (e) => { if (!this.confirmDelete()) { e.preventDefault(); } }); }); } /** * Initialize compose features */ initComposeFeatures() { const form = document.getElementById('composeForm'); if (!form) return; // Auto-resize textarea const textarea = form.querySelector('textarea[name="content"]'); if (textarea) { textarea.addEventListener('input', () => { this.autoResizeTextarea(textarea); }); } // Rich text toolbar const toolbarBtns = document.querySelectorAll('.toolbar-btn'); toolbarBtns.forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); this.handleToolbarAction(btn); }); }); // Save draft button const saveDraftBtn = document.getElementById('saveDraftBtn'); if (saveDraftBtn) { saveDraftBtn.addEventListener('click', () => { this.saveDraft(); }); } // Form submission form.addEventListener('submit', (e) => { this.handleFormSubmit(e); }); } /** * Initialize keyboard shortcuts */ initKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Ctrl/Cmd + K for search if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); const searchInput = document.querySelector('.search-input'); if (searchInput) { searchInput.focus(); } } // Ctrl/Cmd + N for new message if ((e.ctrlKey || e.metaKey) && e.key === 'n') { e.preventDefault(); const composeBtn = document.querySelector('.compose-btn'); if (composeBtn) { window.location.href = composeBtn.href; } } // Escape to close modals if (e.key === 'Escape') { this.closeModals(); } }); } /** * Initialize auto-save functionality */ initAutoSave() { const form = document.getElementById('composeForm'); if (!form) return; let autoSaveTimer; const inputs = form.querySelectorAll('input, textarea, select'); inputs.forEach(input => { input.addEventListener('input', () => { clearTimeout(autoSaveTimer); autoSaveTimer = setTimeout(() => { this.autoSave(); }, 30000); // Auto-save after 30 seconds }); }); } /** * Initialize attachment handling */ initAttachments() { const attachBtn = document.querySelector('.toolbar-btn[title*="Attach"]'); if (attachBtn) { attachBtn.addEventListener('click', () => { this.showAttachmentDialog(); }); } } /** * Initialize tooltips */ initTooltips() { const tooltipElements = document.querySelectorAll('[title]'); tooltipElements.forEach(element => { element.addEventListener('mouseenter', (e) => { this.showTooltip(e.target); }); element.addEventListener('mouseleave', (e) => { this.hideTooltip(e.target); }); }); } /** * Refresh messages with animation */ refreshMessages() { const refreshBtn = document.querySelector('.action-btn[title="Refresh"]'); if (refreshBtn) { const icon = refreshBtn.querySelector('i'); icon.classList.add('fa-spin'); setTimeout(() => { icon.classList.remove('fa-spin'); location.reload(); }, 1000); } } /** * Mark message as read */ async markAsRead(button) { const messageId = button.getAttribute('data-message-id'); if (!messageId) return; try { const response = await fetch(`/messages/${messageId}/mark-read/`, { method: 'POST', headers: { 'X-CSRFToken': this.getCSRFToken(), 'Content-Type': 'application/json', }, }); const data = await response.json(); if (data.success) { this.showNotification('{% trans "Message marked as read" %}', 'success'); location.reload(); } else { this.showNotification('{% trans "Failed to mark message as read" %}', 'error'); } } catch (error) { console.error('Error marking message as read:', error); this.showNotification('{% trans "An error occurred" %}', 'error'); } } /** * Confirm delete action */ confirmDelete() { return confirm('{% trans "Are you sure you want to delete this message?" %}'); } /** * Auto-resize textarea */ autoResizeTextarea(textarea) { textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; } /** * Handle toolbar actions */ handleToolbarAction(button) { const action = button.getAttribute('title'); const textarea = document.querySelector('textarea[name="content"]'); if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); switch (action) { case '{% trans "Bold" %}': this.wrapText(textarea, '**', '**'); break; case '{% trans "Italic" %}': this.wrapText(textarea, '*', '*'); break; case '{% trans "Underline" %}': this.wrapText(textarea, '__', '__'); break; case '{% trans "Bullet List" %}': this.insertList(textarea, '- '); break; case '{% trans "Numbered List" %}': this.insertList(textarea, '1. '); break; case '{% trans "Insert Link" %}': this.insertLink(textarea); break; case '{% trans "Insert Image" %}': this.insertImage(textarea); break; case '{% trans "Attach File" %}': this.showAttachmentDialog(); break; } } /** * Wrap selected text with formatting */ wrapText(textarea, before, after) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); const replacement = before + selectedText + after; textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); textarea.selectionStart = start + before.length; textarea.selectionEnd = start + before.length + selectedText.length; textarea.focus(); } /** * Insert list */ insertList(textarea, marker) { const start = textarea.selectionStart; const text = marker + '\n'; textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(start); textarea.selectionStart = textarea.selectionEnd = start + text.length; textarea.focus(); } /** * Insert link */ insertLink(textarea) { const url = prompt('{% trans "Enter URL:" %}', 'https://'); if (url) { const link = `[${url}](${url})`; this.insertAtCursor(textarea, link); } } /** * Insert image */ insertImage(textarea) { const url = prompt('{% trans "Enter image URL:" %}', 'https://'); if (url) { const image = `![Image](${url})`; this.insertAtCursor(textarea, image); } } /** * Insert text at cursor position */ insertAtCursor(textarea, text) { const start = textarea.selectionStart; textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(start); textarea.selectionStart = textarea.selectionEnd = start + text.length; textarea.focus(); } /** * Save draft */ async saveDraft() { const form = document.getElementById('composeForm'); if (!form) return; const formData = new FormData(form); try { const response = await fetch('/messages/save-draft/', { method: 'POST', body: formData, headers: { 'X-CSRFToken': this.getCSRFToken(), }, }); const data = await response.json(); if (data.success) { this.showNotification('{% trans "Draft saved" %}', 'success'); } else { this.showNotification('{% trans "Failed to save draft" %}', 'error'); } } catch (error) { console.error('Error saving draft:', error); this.showNotification('{% trans "An error occurred" %}', 'error'); } } /** * Auto-save draft */ async autoSave() { const form = document.getElementById('composeForm'); if (!form) return; // Only auto-save if form has content const hasContent = form.querySelector('textarea[name="content"]').value.trim() || form.querySelector('input[name="subject"]').value.trim(); if (!hasContent) return; try { const formData = new FormData(form); await fetch('/messages/auto-save-draft/', { method: 'POST', body: formData, headers: { 'X-CSRFToken': this.getCSRFToken(), }, }); console.log('Draft auto-saved'); } catch (error) { console.error('Error auto-saving draft:', error); } } /** * Handle form submission */ handleFormSubmit(e) { const form = e.target; const submitBtn = form.querySelector('button[type="submit"]'); if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '{% trans "Sending..." %}'; } } /** * Show attachment dialog */ showAttachmentDialog() { const input = document.createElement('input'); input.type = 'file'; input.multiple = true; input.accept = 'image/*,.pdf,.doc,.docx,.txt'; input.addEventListener('change', (e) => { this.handleFileSelect(e.target.files); }); input.click(); } /** * Handle file selection */ handleFileSelect(files) { const attachmentsSection = document.getElementById('attachmentsSection'); const attachmentList = document.getElementById('attachmentList'); if (!attachmentsSection || !attachmentList) return; attachmentsSection.style.display = 'block'; Array.from(files).forEach(file => { const attachmentItem = this.createAttachmentItem(file); attachmentList.appendChild(attachmentItem); }); } /** * Create attachment item */ createAttachmentItem(file) { const item = document.createElement('div'); item.className = 'attachment-item'; const icon = this.getFileIcon(file.type); const size = this.formatFileSize(file.size); item.innerHTML = ` ${file.name} ${size} `; return item; } /** * Get file icon based on MIME type */ getFileIcon(mimeType) { if (mimeType.startsWith('image/')) return 'fa-image'; if (mimeType.includes('pdf')) return 'fa-file-pdf'; if (mimeType.includes('word')) return 'fa-file-word'; if (mimeType.includes('text')) return 'fa-file-alt'; return 'fa-file'; } /** * Format file size */ formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Show loading state */ showLoadingState() { const messagesList = document.querySelector('.messages-list'); if (messagesList) { messagesList.style.opacity = '0.5'; } } /** * Show notification */ showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.innerHTML = ` ${message} `; document.body.appendChild(notification); setTimeout(() => { notification.classList.add('show'); }, 100); setTimeout(() => { notification.classList.remove('show'); setTimeout(() => { notification.remove(); }, 300); }, 3000); } /** * Get notification icon */ getNotificationIcon(type) { const icons = { success: 'fa-check-circle', error: 'fa-exclamation-circle', warning: 'fa-exclamation-triangle', info: 'fa-info-circle' }; return icons[type] || icons.info; } /** * Show tooltip */ showTooltip(element) { const title = element.getAttribute('title'); if (!title) return; const tooltip = document.createElement('div'); tooltip.className = 'tooltip'; tooltip.textContent = title; document.body.appendChild(tooltip); const rect = element.getBoundingClientRect(); tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px'; tooltip.style.top = rect.top - tooltip.offsetHeight - 5 + 'px'; element.setAttribute('data-original-title', title); element.removeAttribute('title'); } /** * Hide tooltip */ hideTooltip(element) { const tooltip = document.querySelector('.tooltip'); if (tooltip) { tooltip.remove(); } const originalTitle = element.getAttribute('data-original-title'); if (originalTitle) { element.setAttribute('title', originalTitle); element.removeAttribute('data-original-title'); } } /** * Close modals */ closeModals() { const modals = document.querySelectorAll('.modal'); modals.forEach(modal => { modal.style.display = 'none'; }); } /** * Get CSRF token */ getCSRFToken() { const cookie = document.cookie.split(';').find(c => c.trim().startsWith('csrftoken=')); return cookie ? cookie.split('=')[1] : ''; } /** * Initialize reply functionality */ initReplyFunctionality() { // Handle reply button clicks const replyBtns = document.querySelectorAll('.reply-btn'); replyBtns.forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); this.handleReplyClick(btn); }); }); // Handle reply form submissions const replyForms = document.querySelectorAll('#reply-form'); replyForms.forEach(form => { form.addEventListener('submit', (e) => { e.preventDefault(); this.handleReplySubmit(form); }); }); // Handle cancel button in reply forms const cancelBtns = document.querySelectorAll('[onclick*="hideReplyForm"]'); cancelBtns.forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); this.hideReplyForm(btn.closest('#reply-section')); }); }); } /** * Handle reply button click */ async handleReplyClick(button) { const messageId = button.getAttribute('data-message-id'); if (!messageId) return; try { // Show loading state const originalText = button.innerHTML; button.innerHTML = ' Loading...'; button.disabled = true; // Fetch reply form via AJAX const response = await fetch(`/messages/${messageId}/reply/`, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': this.getCSRFToken(), }, }); const data = await response.json(); if (data.success) { // Show reply form const replySection = document.getElementById('reply-section'); if (replySection) { replySection.innerHTML = data.html; // Focus on the textarea const textarea = replySection.querySelector('textarea[name="content"]'); if (textarea) { setTimeout(() => textarea.focus(), 100); } } } else { this.showNotification('Failed to load reply form', 'error'); } // Restore button state button.innerHTML = originalText; button.disabled = false; } catch (error) { console.error('Error loading reply form:', error); this.showNotification('An error occurred while loading reply form', 'error'); // Restore button state button.innerHTML = originalText; button.disabled = false; } } /** * Handle reply form submission */ async handleReplySubmit(form) { const submitBtn = form.querySelector('button[type="submit"]'); const textarea = form.querySelector('textarea[name="content"]'); const content = textarea.value.trim(); if (!content) { this.showNotification('Reply content cannot be empty', 'error'); textarea.focus(); return; } try { // Show loading state const originalText = submitBtn.innerHTML; submitBtn.innerHTML = ' Sending...'; submitBtn.disabled = true; const formData = new FormData(form); const response = await fetch(form.action, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': this.getCSRFToken(), }, }); const data = await response.json(); if (data.success) { this.showNotification(data.message, 'success'); // Add the reply to the conversation (if on detail page) this.addReplyToConversation(data); // Hide the reply form this.hideReplyForm(form.closest('#reply-section')); } else { this.showNotification(data.error || 'Failed to send reply', 'error'); } // Restore button state submitBtn.innerHTML = originalText; submitBtn.disabled = false; } catch (error) { console.error('Error sending reply:', error); this.showNotification('An error occurred while sending reply', 'error'); // Restore button state submitBtn.innerHTML = originalText; submitBtn.disabled = false; } } /** * Add reply to conversation */ addReplyToConversation(replyData) { const conversationContainer = document.querySelector('.conversation-container'); if (!conversationContainer) return; // Create new reply element const replyElement = document.createElement('div'); replyElement.className = 'message-reply fade-in'; replyElement.innerHTML = `
${replyData.sender_name || 'You'}

Reply

${replyData.reply_time}

${replyData.reply_content}

`; conversationContainer.appendChild(replyElement); // Scroll to the new reply replyElement.scrollIntoView({ behavior: 'smooth', block: 'end' }); } /** * Hide reply form */ hideReplyForm(replySection) { if (replySection) { replySection.innerHTML = ''; } } } // Initialize the message system when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.messageSystem = new MessageSystem(); // Initialize reply functionality if (window.messageSystem) { window.messageSystem.initReplyFunctionality(); } }); // Add notification styles const notificationStyles = ` .notification { position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px; color: white; font-weight: 500; z-index: 9999; transform: translateX(100%); transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .notification.show { transform: translateX(0); } .notification-success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); } .notification-error { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); } .notification-warning { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); } .notification-info { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); } .tooltip { position: absolute; background: #1f2937; color: white; padding: 6px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 9999; pointer-events: none; } .tooltip::after { content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: #1f2937; } `; // Add styles to head const styleSheet = document.createElement('style'); styleSheet.textContent = notificationStyles; document.head.appendChild(styleSheet);