Fix comments: remove 1s queue, direct fetch, optimistic rendering

The queue (COMMENT_REQUEST_DELAY=1000ms) was causing all comment loads to
wait 1 second regardless of context. Comments are always user-triggered
(toggle click, modal open, post submit) so batching was pointless overhead.

- loadComments() now does a direct async fetch — no queue, no delay
- submitComment() optimistically renders the new comment instantly (faded)
  then replaces with real server data once the POST confirms
- Remove batchLoadComments, queueCommentLoad, updateCommentCount, and all
  queue machinery (commentRequestQueue, commentRequestTimeout, etc.)
- Toggle arrow direction preserved during count updates
- All containers updated via querySelectorAll (feed card + modal)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
King Omar 2026-06-27 03:26:17 +10:00
parent 89c548bb86
commit 7f004104e2

View file

@ -2423,12 +2423,41 @@ function toggleComments(proposalId, button) {
}
// Function to load comments for a proposal (feed cards and modals)
function loadComments(proposalId) {
async function loadComments(proposalId) {
// Show loading state immediately in all matching containers (feed + modal)
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (content) content.innerHTML = `<div class="comments-loading">Loading comments...</div>`;
if (content) content.innerHTML = `<div class="comments-loading">Loading...</div>`;
});
queueCommentLoad(proposalId);
const userId = currentUser?.id || 'anonymous';
try {
const res = await fetch(
`${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${userId}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, mode: 'cors', credentials: 'same-origin' }
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (content) renderComments(content, data);
});
// Update toggle arrow + count, preserving open/closed arrow direction
const toggle = document.getElementById(`comments-toggle-${proposalId}`);
if (toggle) {
const arrow = toggle.querySelector('.comments-toggle-icon')?.textContent || '▼';
const count = Array.isArray(data) ? data.length : 0;
toggle.innerHTML = `<span class="comments-toggle-icon">${arrow}</span> ${count} ${count === 1 ? 'comment' : 'comments'}`;
}
} catch (err) {
console.error('Error loading comments:', err);
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (content) content.innerHTML = `<div class="no-comments">Could not load comments.</div>`;
});
}
}
// Function to render comments
@ -2489,138 +2518,57 @@ function formatCommentText(text) {
return withLinks;
}
// Function to submit a new comment
async function submitComment(proposalId, commentText) {
// If commentText is an element, extract the value (backward compatibility)
if (typeof commentText !== 'string') {
const form = commentText;
const commentInput = form.querySelector('.comment-input');
commentText = commentInput.value.trim();
// Disable form while submitting
commentInput.disabled = true;
const submitButton = form.querySelector('.comment-submit');
submitButton.disabled = true;
// Enable form when done
const enableForm = () => {
commentInput.disabled = false;
submitButton.disabled = false;
};
// Return early if empty
if (!commentText) {
enableForm();
return;
}
// Clear input optimistically
commentInput.value = '';
}
// Ensure we have text to submit
if (!commentText || !commentText.trim()) {
console.error('Empty comment text');
return;
}
console.log(`Submitting comment for proposal ${proposalId}: ${commentText.substring(0, 20)}...`);
if (!commentText || !commentText.trim()) return;
if (!currentUser || !currentUser.id) await loadUserData();
// Optimistically render the comment immediately so it appears instant
const optimisticComment = {
id: 'optimistic_' + Date.now(),
user_name: currentUser.name || 'You',
comment_text: commentText,
timestamp: Date.now(),
upvotes: 0,
downvotes: 0,
userVote: null
};
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (!content) return;
const noComments = content.querySelector('.no-comments');
if (noComments) content.innerHTML = '';
const div = document.createElement('div');
div.className = 'comment-item optimistic';
div.style.opacity = '0.6';
div.innerHTML = `
<div class="comment-header">
<div class="comment-author">${optimisticComment.user_name}</div>
<div class="comment-time">just now</div>
</div>
<div class="comment-text">${formatCommentText(commentText)}</div>`;
content.prepend(div);
});
try {
console.log("Making POST request to comments endpoint");
// Log the full API URL and request details
const requestUrl = `${API_BASE_URL}/comments`;
const requestData = {
proposalId: proposalId,
userId: currentUser?.id || 'anonymous',
commentText: commentText
};
console.log("Request URL:", requestUrl);
console.log("Request data:", requestData);
// Ensure we have a user
if (!currentUser || !currentUser.id) {
console.error("No current user - creating anonymous");
showToastMessage('Please wait, initializing user...', '#ff0099');
await loadUserData(); // Reinitialize user
}
// Create a controller to manage request timeouts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
const response = await fetch(requestUrl, {
const res = await fetch(`${API_BASE_URL}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
proposalId: proposalId,
userId: currentUser.id,
commentText: commentText
}),
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ proposalId, userId: currentUser.id, commentText }),
mode: 'cors',
credentials: 'same-origin',
signal: controller.signal
credentials: 'same-origin'
});
clearTimeout(timeoutId);
console.log("Comment submission response status:", response.status);
console.log("Response headers:", Object.fromEntries(response.headers.entries()));
// Process response based on status code
if (response.status === 200 || response.status === 201) {
// Successfully created comment
try {
const data = await response.json();
console.log("Comment created successfully:", data);
loadComments(proposalId);
updateCommentCount(proposalId);
showToastMessage('Comment posted!', '#11cc77');
return data;
} catch (jsonError) {
console.warn("Could not parse success response as JSON:", jsonError);
// Still consider this a success if status code was good
loadComments(proposalId);
updateCommentCount(proposalId);
showToastMessage('Comment posted!', '#11cc77');
return { success: true };
}
} else {
// Server returned an error
let errorMessage;
try {
const errorData = await response.json();
console.error("Error response data:", errorData);
errorMessage = errorData.error || 'Unknown server error';
} catch (e) {
try {
errorMessage = await response.text() || 'Failed to post comment';
console.error("Error response text:", errorMessage);
} catch (textError) {
errorMessage = `HTTP error ${response.status}`;
console.error("Could not read error response:", textError);
}
}
console.error('Error posting comment:', errorMessage);
showToastMessage('Failed to post comment', '#ff0099');
throw new Error(errorMessage);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${res.status}`);
}
} catch (error) {
if (error.name === 'AbortError') {
console.error('Comment request timed out');
showToastMessage('Comment request timed out', '#ff0099');
} else {
console.error('Network or processing error:', error);
showToastMessage('Failed to post comment', '#ff0099');
}
throw error;
// Replace optimistic with real data from server
loadComments(proposalId);
showToastMessage('Comment posted!', '#11cc77');
} catch (err) {
// Remove the optimistic comment on failure
document.querySelectorAll('.comment-item.optimistic').forEach(el => el.remove());
showToastMessage('Failed to post comment', '#ff0099');
throw err;
}
}
@ -2890,80 +2838,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Add these functions outside your renderProposals function
function updateCommentCount(proposalId) {
const commentsToggle = document.getElementById(`comments-toggle-${proposalId}`);
if (!commentsToggle) return;
safeFetch(`${API_BASE_URL}/comments?proposalId=${proposalId}`)
.then(comments => {
const commentCount = Array.isArray(comments) ? comments.length : 0;
const commentText = commentCount === 1 ? 'comment' : 'comments';
commentsToggle.innerHTML = `<span class="comments-toggle-icon"></span> ${commentCount} ${commentText}`;
})
.catch(() => {
commentsToggle.innerHTML = `<span class="comments-toggle-icon"></span> 0 comments`;
});
}
// Add these new functions for comment management
const commentRequestQueue = new Set();
let commentRequestTimeout = null;
const MAX_BATCH_SIZE = 10;
const COMMENT_REQUEST_DELAY = 1000; // 1 second delay between batches
async function batchLoadComments() {
commentRequestTimeout = null; // reset so the next queueCommentLoad can schedule again
if (commentRequestQueue.size === 0) return;
const batch = Array.from(commentRequestQueue).slice(0, MAX_BATCH_SIZE);
commentRequestQueue.clear();
const userId = currentUser?.id || 'anonymous';
await Promise.all(batch.map(async proposalId => {
try {
const response = await fetch(
`${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${userId}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, mode: 'cors', credentials: 'same-origin' }
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
// Update ALL containers for this proposal (covers both feed card and modal)
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (content) renderComments(content, data);
});
// Update toggle count on feed card
const toggle = document.getElementById(`comments-toggle-${proposalId}`);
if (toggle) {
const count = Array.isArray(data) ? data.length : 0;
toggle.innerHTML = `<span class="comments-toggle-icon"></span> ${count} ${count === 1 ? 'comment' : 'comments'}`;
}
} catch (err) {
console.error(`Failed to load comments for ${proposalId}:`, err);
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (content) content.innerHTML = `<div class="no-comments">Could not load comments.</div>`;
});
}
}));
if (commentRequestQueue.size > 0) {
commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY);
}
}
function queueCommentLoad(proposalId) {
commentRequestQueue.add(proposalId);
if (!commentRequestTimeout) {
commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY);
}
}
// ─── STATS BAR ────────────────────────────────────────────────────────────
async function fetchStats() {