950 lines
28 KiB
JavaScript
950 lines
28 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="text-center py-8">
|
|
<i class="fas fa-search text-4xl text-gray-400 mb-4"></i>
|
|
<h3 class="text-lg font-semibold text-gray-700 mb-2">
|
|
{% trans "No messages found" %}
|
|
</h3>
|
|
<p class="text-gray-500">
|
|
{% trans "No messages match your search for" %} "${searchTerm}"
|
|
</p>
|
|
</div>
|
|
`;
|
|
|
|
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 = ``;
|
|
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 = '<i class="fas fa-spinner fa-spin me-1"></i>{% 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 = `
|
|
<i class="fas ${icon} attachment-icon"></i>
|
|
<span class="attachment-name">${file.name}</span>
|
|
<span class="attachment-size">${size}</span>
|
|
<i class="fas fa-times attachment-remove" onclick="this.parentElement.remove()"></i>
|
|
`;
|
|
|
|
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 = `
|
|
<i class="fas ${this.getNotificationIcon(type)} me-2"></i>
|
|
${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 = '<i class="fas fa-spinner fa-spin me-1"></i> 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 = '<i class="fas fa-spinner fa-spin me-1"></i> 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 = `
|
|
<div class="flex items-start space-x-3 mb-4">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center">
|
|
<span class="text-white text-sm font-medium">${replyData.sender_name || 'You'}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow">
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h4 class="text-sm font-medium text-gray-900">Reply</h4>
|
|
<span class="text-xs text-gray-500">${replyData.reply_time}</span>
|
|
</div>
|
|
<p class="text-sm text-gray-700 whitespace-pre-wrap">${replyData.reply_content}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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);
|