Browse Source

fix comments on articles

master^2
Silberengel 2 months ago
parent
commit
de16df85eb
  1. 289
      src/lib/components/publications/HighlightLayer.svelte
  2. 70
      src/lib/components/publications/Publication.svelte
  3. 31
      src/lib/components/publications/PublicationSection.svelte
  4. 88
      src/lib/components/publications/SectionComments.svelte

289
src/lib/components/publications/HighlightLayer.svelte

@ -52,12 +52,13 @@ @@ -52,12 +52,13 @@
let copyFeedback = $state<string | null>(null);
// Derived state for color mapping
// AI-NOTE: Increased opacity from 0.3 to 0.5 for better visibility
let colorMap = $derived.by(() => {
const map = new Map<string, string>();
highlights.forEach((highlight) => {
if (!map.has(highlight.pubkey)) {
const hue = pubkeyToHue(highlight.pubkey);
map.set(highlight.pubkey, `hsla(${hue}, 70%, 60%, 0.3)`);
map.set(highlight.pubkey, `hsla(${hue}, 70%, 60%, 0.5)`);
}
});
return map;
@ -281,34 +282,102 @@ @@ -281,34 +282,102 @@
const sectionElement = document.getElementById(targetAddress);
if (sectionElement) {
searchRoot = sectionElement;
console.debug(`[HighlightLayer] Found section element for ${targetAddress}, using it as search root`);
} else {
console.warn(`[HighlightLayer] Section element not found for ${targetAddress}, falling back to containerRef`);
}
}
return highlightByOffset(searchRoot, offsetStart, offsetEnd, color);
const result = highlightByOffset(searchRoot, offsetStart, offsetEnd, color);
if (!result) {
console.warn(`[HighlightLayer] highlightByOffset returned false for offsets ${offsetStart}-${offsetEnd} in ${targetAddress || 'container'}`);
}
return result;
}
// Track retry attempts for text highlighting
const textHighlightRetries = new Map<string, number>();
const MAX_TEXT_HIGHLIGHT_RETRIES = 10;
const TEXT_HIGHLIGHT_RETRY_DELAYS = [100, 200, 300, 500, 750, 1000, 1500, 2000, 3000, 5000]; // ms
/**
* Find text in the DOM and highlight it (fallback method)
* @param text - The text to highlight
* @param color - The color to use for highlighting
* @param targetAddress - Optional address to limit search to specific section
* @param retryCount - Internal parameter for retry attempts
*/
function findAndHighlightText(
text: string,
color: string,
targetAddress?: string,
retryCount: number = 0,
): void {
if (!containerRef || !text || text.trim().length === 0) {
console.log(`[HighlightLayer] findAndHighlightText called: text="${text}", color="${color}", targetAddress="${targetAddress}", retryCount=${retryCount}`);
if (!text || text.trim().length === 0) {
console.warn(`[HighlightLayer] Empty text provided, returning`);
return;
}
// If we have a target address, search only in that section
let searchRoot: HTMLElement | Document = containerRef;
// AI-NOTE: When viewing a section directly (leaf), the section element might be outside containerRef
// So we search the entire document for the section element
let searchRoot: HTMLElement | Document | null = null;
let sectionElement: HTMLElement | null = null;
if (targetAddress) {
const sectionElement = document.getElementById(targetAddress);
// Search in entire document, not just containerRef
sectionElement = document.getElementById(targetAddress);
if (sectionElement) {
searchRoot = sectionElement;
// AI-NOTE: The actual content is in a nested <section class="whitespace-normal publication-leather">
// created by contentParagraph snippet. Look for that nested section first.
const contentSection = sectionElement.querySelector('section.publication-leather');
if (contentSection) {
searchRoot = contentSection as HTMLElement;
console.debug(`[HighlightLayer] Found nested content section for ${targetAddress}, searching for text: "${text.substring(0, 50)}"`);
} else {
// Fallback to .section-content div if nested section not found
const contentDiv = sectionElement.querySelector(".section-content");
if (contentDiv) {
searchRoot = contentDiv as HTMLElement;
console.debug(`[HighlightLayer] Found section-content div for ${targetAddress}, searching for text: "${text.substring(0, 50)}"`);
} else {
// Last resort: search entire section
searchRoot = sectionElement;
console.debug(`[HighlightLayer] Found section element for ${targetAddress} (no nested content found), searching for text: "${text.substring(0, 50)}"`);
}
}
} else {
// Section not found - might not be loaded yet, retry if we haven't exceeded max retries
const retryKey = `${targetAddress}:${text}`;
const currentRetries = textHighlightRetries.get(retryKey) || 0;
if (currentRetries < MAX_TEXT_HIGHLIGHT_RETRIES) {
textHighlightRetries.set(retryKey, currentRetries + 1);
const delay = TEXT_HIGHLIGHT_RETRY_DELAYS[Math.min(currentRetries, TEXT_HIGHLIGHT_RETRY_DELAYS.length - 1)];
console.debug(`[HighlightLayer] Section element not found for ${targetAddress}, retrying in ${delay}ms (attempt ${currentRetries + 1}/${MAX_TEXT_HIGHLIGHT_RETRIES})`);
setTimeout(() => {
findAndHighlightText(text, color, targetAddress, retryCount + 1);
}, delay);
return;
} else {
console.warn(`[HighlightLayer] Section element not found for ${targetAddress} after ${MAX_TEXT_HIGHLIGHT_RETRIES} retries, giving up.`);
return;
}
}
} else {
// No target address - use containerRef if available, otherwise document
if (containerRef) {
searchRoot = containerRef;
console.debug(`[HighlightLayer] No target address, searching in containerRef for text: "${text.substring(0, 50)}"`);
} else {
searchRoot = document;
console.debug(`[HighlightLayer] No target address and no containerRef, searching in document for text: "${text.substring(0, 50)}"`);
}
}
if (!searchRoot) {
return;
}
// Use TreeWalker to find all text nodes
@ -318,39 +387,127 @@ @@ -318,39 +387,127 @@
null,
);
// AI-NOTE: First, check if the text exists in the full content
// This helps us know if we should continue searching
const fullText = searchRoot instanceof HTMLElement
? searchRoot.textContent || searchRoot.innerText || ""
: "";
const normalizedSearchText = text.trim().toLowerCase();
const normalizedFullText = fullText.toLowerCase();
// AI-NOTE: If content is empty or very short, the section might still be loading
// Check if we have meaningful content (more than just whitespace/formatting)
const hasContent = fullText.trim().length > 5; // At least 5 characters of actual content
if (!hasContent && sectionElement && retryCount < MAX_TEXT_HIGHLIGHT_RETRIES) {
// Content not loaded yet, retry
const retryKey = `${targetAddress || 'no-address'}:${text}`;
const currentRetries = textHighlightRetries.get(retryKey) || 0;
if (currentRetries < MAX_TEXT_HIGHLIGHT_RETRIES) {
textHighlightRetries.set(retryKey, currentRetries + 1);
const delay = TEXT_HIGHLIGHT_RETRY_DELAYS[Math.min(currentRetries, TEXT_HIGHLIGHT_RETRY_DELAYS.length - 1)];
console.debug(`[HighlightLayer] Section content not loaded yet for ${targetAddress}, retrying in ${delay}ms (attempt ${currentRetries + 1}/${MAX_TEXT_HIGHLIGHT_RETRIES})`);
setTimeout(() => {
findAndHighlightText(text, color, targetAddress, retryCount + 1);
}, delay);
return;
}
}
if (!normalizedFullText.includes(normalizedSearchText)) {
// Text not found - retry if section exists and we haven't exceeded max retries
if (sectionElement && retryCount < MAX_TEXT_HIGHLIGHT_RETRIES) {
const retryKey = `${targetAddress || 'no-address'}:${text}`;
const currentRetries = textHighlightRetries.get(retryKey) || 0;
if (currentRetries < MAX_TEXT_HIGHLIGHT_RETRIES) {
textHighlightRetries.set(retryKey, currentRetries + 1);
const delay = TEXT_HIGHLIGHT_RETRY_DELAYS[Math.min(currentRetries, TEXT_HIGHLIGHT_RETRY_DELAYS.length - 1)];
console.debug(`[HighlightLayer] Text "${text}" not found in content yet, retrying in ${delay}ms (attempt ${currentRetries + 1}/${MAX_TEXT_HIGHLIGHT_RETRIES}). Full text (first 200 chars): "${fullText.substring(0, 200)}"`);
setTimeout(() => {
findAndHighlightText(text, color, targetAddress, retryCount + 1);
}, delay);
return;
}
}
console.warn(
`[HighlightLayer] Text "${text}" not found in full content. Full text (first 200 chars): "${fullText.substring(0, 200)}"`,
);
return;
}
console.debug(
`[HighlightLayer] Text "${text}" found in full content. Searching text nodes...`,
);
// Collect all text nodes
const textNodes: Node[] = [];
let node: Node | null;
while ((node = walker.nextNode())) {
textNodes.push(node);
}
console.debug(`[HighlightLayer] Found ${textNodes.length} text nodes to search`);
// Log first few text nodes for debugging
const sampleNodes = textNodes.slice(0, 20).filter(n => n.textContent && n.textContent.trim().length > 0);
console.log(`[HighlightLayer] Sample text nodes (first 20 non-empty):`, sampleNodes.map(n => `"${n.textContent?.substring(0, 50)}${(n.textContent?.length || 0) > 50 ? '...' : ''}"`));
// Search for the highlight text in text nodes
for (const textNode of textNodes) {
// AI-NOTE: Use simple indexOf for exact matching - the text should match exactly
for (let i = 0; i < textNodes.length; i++) {
const textNode = textNodes[i];
const nodeText = textNode.textContent || "";
const index = nodeText.toLowerCase().indexOf(text.toLowerCase());
if (!nodeText || nodeText.trim().length === 0) {
continue; // Skip empty text nodes
}
// Log every text node that contains the search text (for debugging)
if (nodeText.toLowerCase().includes(text.toLowerCase())) {
console.log(`[HighlightLayer] Text node ${i} contains search text: "${nodeText}"`);
}
// Try exact match first (case-sensitive)
let index = nodeText.indexOf(text);
// If exact match fails, try case-insensitive
if (index === -1) {
const normalizedNodeText = nodeText.toLowerCase();
const normalizedSearchText = text.toLowerCase();
index = normalizedNodeText.indexOf(normalizedSearchText);
}
if (index !== -1) {
const parent = textNode.parentNode;
if (!parent) continue;
if (!parent) {
console.warn(`[HighlightLayer] Text node has no parent, skipping`);
continue;
}
// Skip if already highlighted
if (
parent.nodeName === "MARK" ||
(parent instanceof Element && parent.classList?.contains("highlight"))
) {
console.debug(`[HighlightLayer] Text node already highlighted, skipping`);
continue;
}
// Find the actual match - use the original text length
const matchLength = text.length;
const before = nodeText.substring(0, index);
const match = nodeText.substring(index, index + text.length);
const after = nodeText.substring(index + text.length);
const match = nodeText.substring(index, index + matchLength);
const after = nodeText.substring(index + matchLength);
// Create highlight span
console.debug(`[HighlightLayer] Found match at index ${index}: "${match}" in node: "${nodeText.substring(0, 100)}${nodeText.length > 100 ? '...' : ''}"`);
// Create highlight span with visible styling
const highlightSpan = document.createElement("mark");
highlightSpan.className = "highlight";
highlightSpan.style.backgroundColor = color;
highlightSpan.style.borderRadius = "2px";
highlightSpan.style.padding = "2px 0";
highlightSpan.style.color = "inherit"; // Ensure text color is visible
highlightSpan.style.fontWeight = "inherit"; // Preserve font weight
highlightSpan.textContent = match;
// Replace the text node with the highlighted version
@ -361,16 +518,54 @@ @@ -361,16 +518,54 @@
parent.replaceChild(fragment, textNode);
console.debug(
`[HighlightLayer] Successfully highlighted text "${match}" at index ${index} in node with text: "${nodeText.substring(0, 50)}${nodeText.length > 50 ? "..." : ""}"`,
);
// Clear retry count on success
if (targetAddress) {
const retryKey = `${targetAddress}:${text}`;
textHighlightRetries.delete(retryKey);
}
return; // Only highlight first occurrence to avoid multiple highlights
}
}
// AI-NOTE: If no match found, log for debugging
console.warn(
`[HighlightLayer] Could not find highlight text "${text}" in section. Searched ${textNodes.length} text nodes.`,
);
if (textNodes.length > 0) {
// Log more text nodes and their full content to help debug
const sampleNodes = textNodes.slice(0, 10);
console.debug(
`[HighlightLayer] Sample text nodes (first 10):`,
sampleNodes.map((n, i) => ({
index: i,
text: n.textContent?.substring(0, 100) || "",
fullLength: n.textContent?.length || 0,
parentTag: n.parentElement?.tagName || "unknown",
})),
);
// Also log the full text content of the search root to see what's actually there
if (searchRoot instanceof HTMLElement) {
const fullText = searchRoot.textContent || "";
console.debug(
`[HighlightLayer] Full text content of search root (first 500 chars): "${fullText.substring(0, 500)}"`,
);
console.debug(
`[HighlightLayer] Full text contains "${text}": ${fullText.toLowerCase().includes(text.toLowerCase())}`,
);
}
}
}
/**
* Render all highlights on the page
*/
function renderHighlights() {
if (!visible || !containerRef) {
if (!visible) {
return;
}
@ -378,17 +573,36 @@ @@ -378,17 +573,36 @@
return;
}
// Clear existing highlights
clearHighlights();
// AI-NOTE: When viewing a section directly (leaf), containerRef might not be set
// But we can still highlight by searching the document for section elements
// Only clear highlights if containerRef exists, otherwise clear from document
if (containerRef) {
clearHighlights();
} else {
// Clear highlights from entire document
const highlightElements = document.querySelectorAll("mark.highlight");
highlightElements.forEach((el) => {
const parent = el.parentNode;
if (parent) {
const textNode = document.createTextNode(el.textContent || "");
parent.replaceChild(textNode, el);
parent.normalize();
}
});
}
// Apply each highlight
for (const highlight of highlights) {
const content = highlight.content;
const color = colorMap.get(highlight.pubkey) || "hsla(60, 70%, 60%, 0.3)";
const color = colorMap.get(highlight.pubkey) || "hsla(60, 70%, 60%, 0.5)";
console.log(`[HighlightLayer] Processing highlight: content="${content}", color="${color}"`);
// Extract the target address from the highlight's "a" tag
const aTag = highlight.tags.find((tag) => tag[0] === "a");
const targetAddress = aTag ? aTag[1] : undefined;
console.log(`[HighlightLayer] Highlight targetAddress: "${targetAddress}"`);
// Check for offset tags (position-based highlighting)
const offsetTag = highlight.tags.find((tag) => tag[0] === "offset");
@ -412,10 +626,23 @@ @@ -412,10 +626,23 @@
}
// Check if any highlights were actually rendered
const renderedHighlights = containerRef.querySelectorAll("mark.highlight");
const renderedHighlights = containerRef
? containerRef.querySelectorAll("mark.highlight")
: document.querySelectorAll("mark.highlight");
console.log(
`[HighlightLayer] Rendered ${renderedHighlights.length} highlight marks in DOM`,
);
// AI-NOTE: Debug logging to help diagnose highlight visibility issues
if (renderedHighlights.length === 0 && highlights.length > 0) {
console.warn(`[HighlightLayer] No highlights rendered despite ${highlights.length} highlights available. Container:`, containerRef, "Visible:", visible);
// Log highlight details for debugging
highlights.forEach((h, i) => {
const aTag = h.tags.find((tag) => tag[0] === "a");
const offsetTag = h.tags.find((tag) => tag[0] === "offset");
console.debug(`[HighlightLayer] Highlight ${i}: content="${h.content.substring(0, 50)}", targetAddress="${aTag?.[1]}", hasOffset=${!!offsetTag}`);
});
}
}
/**
@ -473,12 +700,32 @@ @@ -473,12 +700,32 @@
});
// Watch for visibility AND highlights changes - render when both are ready
// AI-NOTE: Also watch containerRef to ensure it's set before rendering
$effect(() => {
// This effect runs when either visible or highlights.length changes
// This effect runs when either visible, highlights.length, or containerRef changes
const highlightCount = highlights.length;
if (visible && highlightCount > 0) {
renderHighlights();
if (visible && highlightCount > 0 && containerRef) {
// AI-NOTE: Retry rendering with increasing delays to handle async content loading
// This is especially important when viewing sections directly
let retryCount = 0;
const maxRetries = 5;
const retryDelays = [100, 300, 500, 1000, 2000];
const tryRender = () => {
renderHighlights();
const renderedCount = containerRef?.querySelectorAll("mark.highlight").length || 0;
if (renderedCount === 0 && retryCount < maxRetries) {
console.debug(`[HighlightLayer] No highlights rendered, retrying (attempt ${retryCount + 1}/${maxRetries})...`);
setTimeout(tryRender, retryDelays[retryCount]);
retryCount++;
} else if (renderedCount > 0) {
console.debug(`[HighlightLayer] Successfully rendered ${renderedCount} highlights after ${retryCount} retries`);
}
};
tryRender();
} else if (!visible) {
clearHighlights();
}
@ -643,7 +890,7 @@ @@ -643,7 +890,7 @@
{@const isExpanded = expandedAuthors.has(pubkey)}
{@const profile = authorProfiles.get(pubkey)}
{@const displayName = getAuthorDisplayName(profile, pubkey)}
{@const color = colorMap.get(pubkey) || "hsla(60, 70%, 60%, 0.3)"}
{@const color = colorMap.get(pubkey) || "hsla(60, 70%, 60%, 0.5)"}
{@const sortedHighlights = sortHighlightsByTime(authorHighlights)}
<div class="border-b border-gray-200 dark:border-gray-700 pb-2">

70
src/lib/components/publications/Publication.svelte

@ -108,11 +108,13 @@ @@ -108,11 +108,13 @@
});
// Filter comments for the root publication (kind 30040)
// AI-NOTE: NIP-22: Uppercase A tag points to root scope (publication/section)
// Use uppercase A tag to match comments scoped to the root publication
let articleComments = $derived(
comments.filter((comment) => {
// Check if comment targets the root publication via #a tag
const aTag = comment.tags.find((t) => t[0] === "a");
return aTag && aTag[1] === rootAddress;
// NIP-22: Look for uppercase A tag (root scope)
const rootATag = comment.tags.find((t) => t[0] === "A");
return rootATag && rootATag[1] === rootAddress;
}),
);
@ -1315,25 +1317,16 @@ @@ -1315,25 +1317,16 @@
{/if}
</div>
<!-- Mobile article comments - shown below header on smaller screens -->
<div class="xl:hidden mt-4 max-w-4xl mx-auto px-4">
<SectionComments
sectionAddress={rootAddress}
comments={articleComments}
visible={commentsVisible}
/>
</div>
<!-- Desktop article comments - positioned on right side on XL+ screens -->
<div
class="hidden xl:block absolute left-[calc(50%+26rem)] top-0 w-[max(16rem,min(24rem,calc(50vw-26rem-2rem)))]"
>
<SectionComments
sectionAddress={rootAddress}
comments={articleComments}
visible={commentsVisible}
/>
</div>
<!-- Article comments - shown below header only when viewing full publication (not a section directly) -->
{#if !currentBlog && !isLeaf}
<div class="mt-4 max-w-4xl mx-auto px-4">
<SectionComments
sectionAddress={rootAddress}
comments={articleComments}
visible={commentsVisible}
/>
</div>
{/if}
</div>
@ -1352,6 +1345,7 @@ @@ -1352,6 +1345,7 @@
placeholder="Write your comment on this article..."
rows={4}
disabled={isSubmittingArticleComment}
class="w-full"
/>
{#if articleCommentError}
@ -1421,6 +1415,7 @@ @@ -1421,6 +1415,7 @@
{commentsVisible}
publicationTitle={publicationTitle}
{isFirstSection}
onCommentPosted={handleCommentPosted}
ref={(el) => onPublicationSectionMounted(el, address)}
/>
{/if}
@ -1547,20 +1542,23 @@ @@ -1547,20 +1542,23 @@
active={true}
/>
{/if}
<div class="flex flex-col w-full space-y-4">
<SectionComments
sectionAddress={rootAddress}
comments={articleComments}
visible={commentsVisible}
/>
{#if articleComments.length === 0}
<p
class="text-sm text-gray-500 dark:text-gray-400 text-center py-4"
>
No comments yet. Be the first to comment!
</p>
{/if}
</div>
<!-- Article comments in discussion sidebar - only show when viewing full publication (not a section directly) -->
{#if !currentBlog && !isLeaf}
<div class="flex flex-col w-full space-y-4">
<SectionComments
sectionAddress={rootAddress}
comments={articleComments}
visible={commentsVisible}
/>
{#if articleComments.length === 0}
<p
class="text-sm text-gray-500 dark:text-gray-400 text-center py-4"
>
No comments yet. Be the first to comment!
</p>
{/if}
</div>
{/if}
</div>
</SidebarGroup>
</SidebarWrapper>

31
src/lib/components/publications/PublicationSection.svelte

@ -116,6 +116,19 @@ @@ -116,6 +116,19 @@
const filtered = Array.from(allMatchingComments);
console.debug(`[PublicationSection] Filtered ${filtered.length} comments (${directComments.length} direct, ${filtered.length - directComments.length} replies) for section ${address} from ${allComments.length} total comments`);
// AI-NOTE: Debug logging to check for nested replies in filtered comments
const filteredCommentIds = new Set(filtered.map(c => c.id?.toLowerCase()).filter(Boolean));
for (const comment of filtered) {
const lowercaseETags = comment.tags.filter(t => t[0] === "e");
for (const eTag of lowercaseETags) {
const parentId = eTag[1]?.toLowerCase();
if (parentId && filteredCommentIds.has(parentId)) {
console.debug(`[PublicationSection] Found nested reply ${comment.id?.substring(0, 8)} to filtered comment ${parentId.substring(0, 8)}`);
}
}
}
return filtered;
});
@ -339,8 +352,8 @@ @@ -339,8 +352,8 @@
)}
</div>
<!-- Mobile comments - shown below content on smaller screens -->
<div class="xl:hidden mt-8 w-full text-left">
<!-- Comments - shown below content on all screens -->
<div class="mt-8 w-full text-left">
<SectionComments
sectionAddress={address}
comments={sectionComments}
@ -349,20 +362,6 @@ @@ -349,20 +362,6 @@
</div>
{/await}
</section>
<!-- Comments area: positioned to the right of section on desktop -->
<!-- AI-NOTE: Comments panel positioned to the right of sections on desktop (xl+ screens)
Positioned relative to viewport right edge to ensure visibility -->
<div
class="hidden xl:block fixed right-8 top-[calc(20%+70px)] w-80 max-h-[calc(100vh-200px)] overflow-y-auto z-30"
>
<SectionComments
sectionAddress={address}
comments={sectionComments}
visible={commentsVisible}
/>
</div>
</div>
<style>

88
src/lib/components/publications/SectionComments.svelte

@ -36,6 +36,16 @@ @@ -36,6 +36,16 @@
// Subscribe to userStore
let user = $derived($userStore);
// AI-NOTE: Debug logging to track component rendering and comment reception
$effect(() => {
console.debug(`[SectionComments] Component rendered/re-rendered:`, {
sectionAddress,
commentsCount: comments.length,
visible,
commentIds: comments.map(c => c.id?.substring(0, 8)),
});
});
/**
* Parse comment threading structure according to NIP-22
* NIP-22: Uppercase tags (A, E, I, K, P) = root scope
@ -54,7 +64,14 @@ @@ -54,7 +64,14 @@
// NIP-22: First pass - identify replies by looking for lowercase e tags
// Lowercase e tags point to the parent comment ID
// This works for both direct replies and nested replies (replies to replies)
for (const comment of allComments) {
const commentId = comment.id?.toLowerCase();
if (!commentId) {
console.warn(`[SectionComments] Comment missing ID, skipping`);
continue;
}
// NIP-22: Look for lowercase e tag (parent item reference)
const lowercaseETags = comment.tags.filter(t => t[0] === 'e');
@ -69,6 +86,7 @@ @@ -69,6 +86,7 @@
}
// NIP-22: If lowercase e tag points to a comment in our set, it's a reply
// This works for both direct replies (parent is root comment) and nested replies (parent is another reply)
if (allCommentIds.has(parentId)) {
isReply = true;
@ -76,7 +94,11 @@ @@ -76,7 +94,11 @@
repliesByParent.set(parentId, []);
}
repliesByParent.get(parentId)!.push(comment);
console.debug(`[SectionComments] Comment ${comment.id?.substring(0, 8)} is a reply to ${parentId.substring(0, 8)} (NIP-22 lowercase e tag)`);
// Check if this is a nested reply (reply to a reply)
const isNestedReply = !rootComments.some(rc => rc.id?.toLowerCase() === parentId);
const replyType = isNestedReply ? "nested reply (reply to reply)" : "direct reply";
console.debug(`[SectionComments] Comment ${commentId.substring(0, 8)} is a ${replyType} to ${parentId.substring(0, 8)} (NIP-22 lowercase e tag)`);
break; // Found parent, no need to check other e tags
}
}
@ -85,12 +107,12 @@ @@ -85,12 +107,12 @@
// Has lowercase e tag but doesn't reference any comment in our set
// This might be a root comment that references an external event, or malformed
rootComments.push(comment);
console.debug(`[SectionComments] Comment ${comment.id?.substring(0, 8)} is a root comment (lowercase e tag references external event)`);
console.debug(`[SectionComments] Comment ${commentId.substring(0, 8)} is a root comment (lowercase e tag references external event)`);
}
} else {
// No lowercase e tags - this is a root comment
rootComments.push(comment);
console.debug(`[SectionComments] Comment ${comment.id?.substring(0, 8)} is a root comment (no lowercase e tags)`);
console.debug(`[SectionComments] Comment ${commentId.substring(0, 8)} is a root comment (no lowercase e tags)`);
}
}
@ -98,6 +120,14 @@ @@ -98,6 +120,14 @@
// AI-NOTE: Log reply details for debugging
for (const [parentId, replies] of repliesByParent.entries()) {
console.debug(`[SectionComments] Parent ${parentId.substring(0, 8)} has ${replies.length} replies:`, replies.map(r => r.id?.substring(0, 8)));
// Check if any of these replies themselves have replies (nested replies)
for (const reply of replies) {
const replyId = reply.id?.toLowerCase();
if (replyId && repliesByParent.has(replyId)) {
const nestedReplies = repliesByParent.get(replyId)!;
console.debug(`[SectionComments] → Reply ${replyId.substring(0, 8)} has ${nestedReplies.length} nested replies (replies to replies)`);
}
}
}
return { rootComments, repliesByParent };
}
@ -112,6 +142,19 @@ @@ -112,6 +142,19 @@
replyGroups: structure.repliesByParent.size,
visible,
});
// AI-NOTE: Log all parent IDs in the map to help debug nested reply lookup
const allParentIds = Array.from(structure.repliesByParent.keys());
console.debug(`[SectionComments] All parent IDs in repliesByParent map:`, allParentIds.map(id => id.substring(0, 8)));
// Log which replies have nested replies
for (const [parentId, replies] of structure.repliesByParent.entries()) {
for (const reply of replies) {
const replyId = reply.id?.toLowerCase();
if (replyId && structure.repliesByParent.has(replyId)) {
const nested = structure.repliesByParent.get(replyId)!;
console.debug(`[SectionComments] Reply ${replyId.substring(0, 8)} has ${nested.length} nested replies`);
}
}
}
}
return structure;
});
@ -230,13 +273,18 @@ @@ -230,13 +273,18 @@
// AI-NOTE: Debug logging to track reply rendering
if (replies.length > 0) {
console.debug(`[SectionComments] Rendering ${replies.length} replies for parent ${normalizedParentId.substring(0, 8)}`);
console.debug(`[SectionComments] renderReplies: Found ${replies.length} replies for parent ${normalizedParentId.substring(0, 8)} (level ${level})`);
// Log all parent IDs in the map for debugging
const allParentIds = Array.from(repliesMap.keys());
console.debug(`[SectionComments] renderReplies: All parent IDs in map:`, allParentIds.map(id => id.substring(0, 8)));
} else {
// AI-NOTE: Debug when no replies found - check if map has any entries for similar IDs
const allParentIds = Array.from(repliesMap.keys());
const similarIds = allParentIds.filter(id => id.substring(0, 8) === normalizedParentId.substring(0, 8));
if (similarIds.length > 0) {
console.debug(`[SectionComments] No replies found for ${normalizedParentId.substring(0, 8)}, but found similar IDs:`, similarIds.map(id => id.substring(0, 8)));
console.debug(`[SectionComments] renderReplies: No replies found for ${normalizedParentId.substring(0, 8)}, but found similar IDs:`, similarIds.map(id => id.substring(0, 8)));
} else {
console.debug(`[SectionComments] renderReplies: No replies found for ${normalizedParentId.substring(0, 8)}. Map has ${repliesMap.size} entries.`);
}
}
return replies;
@ -412,8 +460,10 @@ @@ -412,8 +460,10 @@
</script>
<!-- AI-NOTE: Debug info for comment display -->
{#if visible && threadStructure.rootComments.length > 0}
{console.debug(`[SectionComments] RENDERING: visible=${visible}, rootComments=${threadStructure.rootComments.length}, totalComments=${comments.length}`)}
{#if visible}
{console.debug(`[SectionComments] RENDERING CHECK: visible=${visible}, rootComments=${threadStructure.rootComments.length}, totalComments=${comments.length}, repliesByParent.size=${threadStructure.repliesByParent.size}`)}
{#if threadStructure.rootComments.length > 0}
{console.debug(`[SectionComments] RENDERING COMMENTS: visible=${visible}, rootComments=${threadStructure.rootComments.length}, totalComments=${comments.length}`)}
<div class="space-y-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg p-4 shadow-lg">
{#each threadStructure.rootComments as rootComment (rootComment.id)}
{@const replyCount = countReplies(rootComment.id, threadStructure.repliesByParent)}
@ -640,7 +690,7 @@ @@ -640,7 +690,7 @@
placeholder="Write your reply..."
rows={3}
disabled={isSubmittingReply}
class="mb-2"
class="w-full mb-2"
/>
{#if replyError}
@ -679,6 +729,14 @@ @@ -679,6 +729,14 @@
{console.debug(`[SectionComments] Rendering ${replyCount} replies for comment ${rootComment.id?.substring(0, 8)}`)}
<div class="pl-4 border-l-2 border-gray-200 dark:border-gray-600 space-y-2">
{#each renderReplies(rootComment.id, threadStructure.repliesByParent) as reply (reply.id)}
{@const replyId = reply.id?.toLowerCase() || ""}
{@const nestedReplies = replyId ? renderReplies(reply.id, threadStructure.repliesByParent) : []}
{console.debug(`[SectionComments] Processing reply ${reply.id?.substring(0, 8)}, nestedReplies.length=${nestedReplies.length}, replyId=${replyId.substring(0, 8)}, checking map for: ${replyId.substring(0, 8)}`)}
{#if nestedReplies.length > 0}
{console.debug(`[SectionComments] ✓ Found ${nestedReplies.length} nested replies for reply ${reply.id?.substring(0, 8)}, rendering them now`)}
{:else}
{console.debug(`[SectionComments] ✗ No nested replies found for reply ${reply.id?.substring(0, 8)}. Map has ${threadStructure.repliesByParent.size} entries.`)}
{/if}
<div class="bg-gray-50 dark:bg-gray-700/30 rounded p-3">
<div class="flex items-center gap-2 mb-2">
<button
@ -775,7 +833,7 @@ @@ -775,7 +833,7 @@
placeholder="Write your reply..."
rows={3}
disabled={isSubmittingReply}
class="mb-2"
class="w-full mb-2"
/>
{#if replyError}
@ -809,8 +867,10 @@ @@ -809,8 +867,10 @@
</div>
{/if}
<!-- Nested replies (one level deep) -->
{#each renderReplies(reply.id, threadStructure.repliesByParent) as nestedReply (nestedReply.id)}
<!-- Nested replies (replies to replies) -->
{#if nestedReplies.length > 0}
{console.debug(`[SectionComments] Rendering ${nestedReplies.length} nested replies for reply ${reply.id?.substring(0, 8)}`)}
{#each nestedReplies as nestedReply (nestedReply.id)}
<div class="ml-4 mt-2 bg-gray-100 dark:bg-gray-600/30 rounded p-2">
<div class="flex items-center gap-2 mb-1">
<button
@ -907,7 +967,7 @@ @@ -907,7 +967,7 @@
placeholder="Write your reply..."
rows={2}
disabled={isSubmittingReply}
class="mb-2 text-xs"
class="w-full mb-2 text-xs"
/>
{#if replyError}
@ -942,6 +1002,7 @@ @@ -942,6 +1002,7 @@
{/if}
</div>
{/each}
{/if}
</div>
{/each}
</div>
@ -952,6 +1013,9 @@ @@ -952,6 +1013,9 @@
</div>
{/each}
</div>
{:else}
{console.debug(`[SectionComments] NOT RENDERING: visible=${visible} but no root comments (totalComments=${comments.length})`)}
{/if}
{/if}
<!-- Details Modal -->

Loading…
Cancel
Save