|
|
|
|
@ -9,55 +9,149 @@
@@ -9,55 +9,149 @@
|
|
|
|
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
|
threadId: string; // The event ID |
|
|
|
|
event?: NostrEvent; // The event itself (optional, used to determine reply types) |
|
|
|
|
threadId: string; // The event ID of the root event |
|
|
|
|
event?: NostrEvent; // The root event itself (optional, used to determine reply types) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let { threadId, event }: Props = $props(); |
|
|
|
|
|
|
|
|
|
let comments = $state<NostrEvent[]>([]); |
|
|
|
|
let kind1Replies = $state<NostrEvent[]>([]); |
|
|
|
|
let yakBacks = $state<NostrEvent[]>([]); |
|
|
|
|
let zapReceipts = $state<NostrEvent[]>([]); |
|
|
|
|
let comments = $state<NostrEvent[]>([]); // kind 1111 |
|
|
|
|
let kind1Replies = $state<NostrEvent[]>([]); // kind 1 replies (should only be for kind 1 events, but some apps use them for everything) |
|
|
|
|
let yakBacks = $state<NostrEvent[]>([]); // kind 1244 (voice replies) |
|
|
|
|
let zapReceipts = $state<NostrEvent[]>([]); // kind 9735 (zap receipts) |
|
|
|
|
let loading = $state(true); |
|
|
|
|
let replyingTo = $state<NostrEvent | null>(null); |
|
|
|
|
|
|
|
|
|
const isKind1 = $derived(event?.kind === 1); |
|
|
|
|
const rootKind = $derived(event?.kind || null); |
|
|
|
|
|
|
|
|
|
onMount(async () => { |
|
|
|
|
await nostrClient.initialize(); |
|
|
|
|
loadComments(); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Reload comments when threadId or event changes |
|
|
|
|
$effect(() => { |
|
|
|
|
if (threadId) { |
|
|
|
|
loadComments(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Get the parent event ID from a reply event |
|
|
|
|
* For kind 1111: checks both E/e and A/a tags (NIP-22) |
|
|
|
|
* For kind 1: checks e tag (NIP-10) |
|
|
|
|
* For kind 1244: checks E/e and A/a tags (follows NIP-22) |
|
|
|
|
* For kind 9735: checks e tag |
|
|
|
|
*/ |
|
|
|
|
function getParentEventId(replyEvent: NostrEvent): string | null { |
|
|
|
|
// For kind 1111, check both uppercase and lowercase E and A tags |
|
|
|
|
if (replyEvent.kind === 1111) { |
|
|
|
|
// Check uppercase E tag first (NIP-22 standard for root) |
|
|
|
|
const eTag = replyEvent.tags.find((t) => t[0] === 'E'); |
|
|
|
|
if (eTag && eTag[1]) { |
|
|
|
|
// If it points to root, check lowercase e for parent |
|
|
|
|
if (eTag[1] === threadId) { |
|
|
|
|
const parentETag = replyEvent.tags.find((t) => t[0] === 'e' && t[1] !== threadId); |
|
|
|
|
if (parentETag && parentETag[1]) return parentETag[1]; |
|
|
|
|
} else { |
|
|
|
|
// E tag points to parent (non-standard but some clients do this) |
|
|
|
|
return eTag[1]; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// Check lowercase e tag |
|
|
|
|
const lowerETag = replyEvent.tags.find((t) => t[0] === 'e' && t[1] !== threadId && t[1] !== replyEvent.id); |
|
|
|
|
if (lowerETag && lowerETag[1]) return lowerETag[1]; |
|
|
|
|
|
|
|
|
|
// Check uppercase A tag (NIP-22 for addressable events) |
|
|
|
|
const aTag = replyEvent.tags.find((t) => t[0] === 'A'); |
|
|
|
|
if (aTag && aTag[1]) { |
|
|
|
|
// If it points to root, check lowercase a for parent |
|
|
|
|
const parentATag = replyEvent.tags.find((t) => t[0] === 'a' && t[1] !== aTag[1]); |
|
|
|
|
if (parentATag && parentATag[1]) { |
|
|
|
|
// Need to find event by address - for now, check if we have it |
|
|
|
|
// This is complex, so we'll handle it in the parent lookup |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// Check lowercase a tag |
|
|
|
|
const lowerATag = replyEvent.tags.find((t) => t[0] === 'a'); |
|
|
|
|
if (lowerATag && lowerATag[1]) { |
|
|
|
|
// Try to find event with matching address |
|
|
|
|
// For now, we'll handle this by checking all events |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// For kind 1, 1244, 9735: check e tag |
|
|
|
|
if (replyEvent.kind === 1 || replyEvent.kind === 1244 || replyEvent.kind === 9735) { |
|
|
|
|
const eTag = replyEvent.tags.find((t) => t[0] === 'e' && t[1] !== replyEvent.id); |
|
|
|
|
if (eTag && eTag[1]) return eTag[1]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Check if a reply event references the root thread |
|
|
|
|
* For kind 1111: checks both E/e and A/a tags (NIP-22) |
|
|
|
|
* For other kinds: checks e tag |
|
|
|
|
*/ |
|
|
|
|
function referencesRoot(replyEvent: NostrEvent): boolean { |
|
|
|
|
if (replyEvent.kind === 1111) { |
|
|
|
|
// Check uppercase E tag (NIP-22 standard for root) |
|
|
|
|
const eTag = replyEvent.tags.find((t) => t[0] === 'E'); |
|
|
|
|
if (eTag && eTag[1] === threadId) return true; |
|
|
|
|
|
|
|
|
|
// Check lowercase e tag (fallback) |
|
|
|
|
const lowerETag = replyEvent.tags.find((t) => t[0] === 'e' && t[1] === threadId); |
|
|
|
|
if (lowerETag) return true; |
|
|
|
|
|
|
|
|
|
// Check A and a tags for addressable events |
|
|
|
|
// If root event has an address (a-tag), check if reply references it |
|
|
|
|
if (event) { |
|
|
|
|
const rootATag = event.tags.find((t) => t[0] === 'a'); |
|
|
|
|
if (rootATag && rootATag[1]) { |
|
|
|
|
const replyATag = replyEvent.tags.find((t) => t[0] === 'A'); |
|
|
|
|
if (replyATag && replyATag[1] === rootATag[1]) return true; |
|
|
|
|
const replyLowerATag = replyEvent.tags.find((t) => t[0] === 'a' && t[1] === rootATag[1]); |
|
|
|
|
if (replyLowerATag) return true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// If no direct reference found, check if parent is root |
|
|
|
|
const parentId = getParentEventId(replyEvent); |
|
|
|
|
return parentId === null || parentId === threadId; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// For other kinds, check e tag |
|
|
|
|
const eTag = replyEvent.tags.find((t) => t[0] === 'e' && t[1] === threadId); |
|
|
|
|
return !!eTag; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadComments() { |
|
|
|
|
loading = true; |
|
|
|
|
try { |
|
|
|
|
const config = nostrClient.getConfig(); |
|
|
|
|
const relays = relayManager.getCommentReadRelays(); |
|
|
|
|
const feedRelays = relayManager.getFeedReadRelays(); |
|
|
|
|
const allRelays = [...new Set([...relays, ...feedRelays])]; |
|
|
|
|
|
|
|
|
|
const replyFilters: any[] = [ |
|
|
|
|
{ kinds: [9735], '#e': [threadId] }, // Zap receipts |
|
|
|
|
{ kinds: [1244], '#e': [threadId] }, // Yak backs (voice replies) |
|
|
|
|
]; |
|
|
|
|
const replyFilters: any[] = []; |
|
|
|
|
|
|
|
|
|
// For kind 1 events, also fetch kind 1 replies |
|
|
|
|
if (isKind1) { |
|
|
|
|
replyFilters.push({ kinds: [1], '#e': [threadId] }); |
|
|
|
|
} |
|
|
|
|
// Always fetch kind 1111 comments - check both e and E tags, and a and A tags |
|
|
|
|
replyFilters.push( |
|
|
|
|
{ kinds: [1111], '#e': [threadId] }, // Lowercase e tag |
|
|
|
|
{ kinds: [1111], '#E': [threadId] }, // Uppercase E tag (NIP-22) |
|
|
|
|
{ kinds: [1111], '#a': [threadId] }, // Lowercase a tag (some clients use wrong tags) |
|
|
|
|
{ kinds: [1111], '#A': [threadId] } // Uppercase A tag (NIP-22 for addressable events) |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// For all events, fetch kind 1111 comments |
|
|
|
|
// For kind 11 threads, use #E and #K tags (NIP-22) |
|
|
|
|
// For other events, use #e tag |
|
|
|
|
if (event?.kind === 11) { |
|
|
|
|
replyFilters.push( |
|
|
|
|
{ kinds: [1111], '#E': [threadId], '#K': ['11'] }, // NIP-22 standard (uppercase) |
|
|
|
|
{ kinds: [1111], '#e': [threadId] } // Fallback (lowercase) |
|
|
|
|
); |
|
|
|
|
} else { |
|
|
|
|
replyFilters.push({ kinds: [1111], '#e': [threadId] }); |
|
|
|
|
} |
|
|
|
|
// For kind 1 events, fetch kind 1 replies |
|
|
|
|
// Also fetch kind 1 replies for any event (some apps use kind 1 for everything) |
|
|
|
|
replyFilters.push({ kinds: [1], '#e': [threadId] }); |
|
|
|
|
|
|
|
|
|
// Fetch yak backs (kind 1244) - voice replies |
|
|
|
|
replyFilters.push({ kinds: [1244], '#e': [threadId] }); |
|
|
|
|
|
|
|
|
|
// Fetch zap receipts (kind 9735) |
|
|
|
|
replyFilters.push({ kinds: [9735], '#e': [threadId] }); |
|
|
|
|
|
|
|
|
|
const allReplies = await nostrClient.fetchEvents( |
|
|
|
|
replyFilters, |
|
|
|
|
@ -65,17 +159,18 @@
@@ -65,17 +159,18 @@
|
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Filter to only replies that reference the root |
|
|
|
|
const rootReplies = allReplies.filter(reply => referencesRoot(reply)); |
|
|
|
|
|
|
|
|
|
// Separate by type |
|
|
|
|
comments = allReplies.filter(e => e.kind === 1111); |
|
|
|
|
kind1Replies = allReplies.filter(e => e.kind === 1); |
|
|
|
|
yakBacks = allReplies.filter(e => e.kind === 1244); |
|
|
|
|
zapReceipts = allReplies.filter(e => e.kind === 9735); |
|
|
|
|
comments = rootReplies.filter(e => e.kind === 1111); |
|
|
|
|
kind1Replies = rootReplies.filter(e => e.kind === 1); |
|
|
|
|
yakBacks = rootReplies.filter(e => e.kind === 1244); |
|
|
|
|
zapReceipts = rootReplies.filter(e => e.kind === 9735); |
|
|
|
|
|
|
|
|
|
// Recursively fetch all nested replies |
|
|
|
|
await fetchNestedReplies(); |
|
|
|
|
|
|
|
|
|
// Fetch zap receipts that reference this thread or any comment/reply |
|
|
|
|
await fetchZapReceipts(); |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading comments:', error); |
|
|
|
|
} finally { |
|
|
|
|
@ -89,49 +184,43 @@
@@ -89,49 +184,43 @@
|
|
|
|
|
const allRelays = [...new Set([...relays, ...feedRelays])]; |
|
|
|
|
let hasNewReplies = true; |
|
|
|
|
let iterations = 0; |
|
|
|
|
const maxIterations = 10; // Prevent infinite loops |
|
|
|
|
const maxIterations = 10; |
|
|
|
|
|
|
|
|
|
// Keep fetching until we have all nested replies |
|
|
|
|
while (hasNewReplies && iterations < maxIterations) { |
|
|
|
|
iterations++; |
|
|
|
|
hasNewReplies = false; |
|
|
|
|
const allReplyIds = new Set([ |
|
|
|
|
...comments.map(c => c.id), |
|
|
|
|
...kind1Replies.map(r => r.id), |
|
|
|
|
...yakBacks.map(y => y.id) |
|
|
|
|
...yakBacks.map(y => y.id), |
|
|
|
|
...zapReceipts.map(z => z.id) |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
if (allReplyIds.size > 0) { |
|
|
|
|
const nestedFilters: any[] = [ |
|
|
|
|
{ kinds: [9735], '#e': Array.from(allReplyIds) }, // Zap receipts |
|
|
|
|
{ kinds: [1244], '#e': Array.from(allReplyIds) }, // Yak backs |
|
|
|
|
// Fetch nested kind 1111 comments - check both e/E and a/A tags |
|
|
|
|
{ kinds: [1111], '#e': Array.from(allReplyIds) }, |
|
|
|
|
{ kinds: [1111], '#E': Array.from(allReplyIds) }, |
|
|
|
|
{ kinds: [1111], '#a': Array.from(allReplyIds) }, |
|
|
|
|
{ kinds: [1111], '#A': Array.from(allReplyIds) }, |
|
|
|
|
// Fetch nested kind 1 replies |
|
|
|
|
{ kinds: [1], '#e': Array.from(allReplyIds) }, |
|
|
|
|
// Fetch nested yak backs |
|
|
|
|
{ kinds: [1244], '#e': Array.from(allReplyIds) }, |
|
|
|
|
// Fetch nested zap receipts |
|
|
|
|
{ kinds: [9735], '#e': Array.from(allReplyIds) } |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
// For kind 1 events, also fetch nested kind 1 replies |
|
|
|
|
if (isKind1) { |
|
|
|
|
nestedFilters.push({ kinds: [1], '#e': Array.from(allReplyIds) }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fetch nested comments |
|
|
|
|
if (event?.kind === 11) { |
|
|
|
|
nestedFilters.push( |
|
|
|
|
{ kinds: [1111], '#E': Array.from(allReplyIds), '#K': ['11'] }, |
|
|
|
|
{ kinds: [1111], '#e': Array.from(allReplyIds) } |
|
|
|
|
); |
|
|
|
|
} else { |
|
|
|
|
nestedFilters.push({ kinds: [1111], '#e': Array.from(allReplyIds) }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const nestedReplies = await nostrClient.fetchEvents( |
|
|
|
|
nestedFilters, |
|
|
|
|
allRelays, |
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Add new replies by type |
|
|
|
|
for (const reply of nestedReplies) { |
|
|
|
|
if (reply.kind === 1111 && !comments.some(c => c.id === reply.id)) { |
|
|
|
|
comments.push(reply); |
|
|
|
|
comments.push(reply); |
|
|
|
|
hasNewReplies = true; |
|
|
|
|
} else if (reply.kind === 1 && !kind1Replies.some(r => r.id === reply.id)) { |
|
|
|
|
kind1Replies.push(reply); |
|
|
|
|
@ -139,114 +228,42 @@
@@ -139,114 +228,42 @@
|
|
|
|
|
} else if (reply.kind === 1244 && !yakBacks.some(y => y.id === reply.id)) { |
|
|
|
|
yakBacks.push(reply); |
|
|
|
|
hasNewReplies = true; |
|
|
|
|
} else if (reply.kind === 9735 && !zapReceipts.some(z => z.id === reply.id)) { |
|
|
|
|
zapReceipts.push(reply); |
|
|
|
|
hasNewReplies = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchZapReceipts() { |
|
|
|
|
const config = nostrClient.getConfig(); |
|
|
|
|
const relays = relayManager.getCommentReadRelays(); |
|
|
|
|
const feedRelays = relayManager.getFeedReadRelays(); |
|
|
|
|
const allRelays = [...new Set([...relays, ...feedRelays])]; |
|
|
|
|
/** |
|
|
|
|
* Get parent event from any of our loaded events |
|
|
|
|
*/ |
|
|
|
|
function getParentEvent(replyEvent: NostrEvent): NostrEvent | undefined { |
|
|
|
|
const parentId = getParentEventId(replyEvent); |
|
|
|
|
if (!parentId) return undefined; |
|
|
|
|
|
|
|
|
|
// Keep fetching until we have all zaps |
|
|
|
|
let previousCount = -1; |
|
|
|
|
while (zapReceipts.length !== previousCount) { |
|
|
|
|
previousCount = zapReceipts.length; |
|
|
|
|
const allEventIds = new Set([ |
|
|
|
|
threadId, |
|
|
|
|
...comments.map(c => c.id), |
|
|
|
|
...kind1Replies.map(r => r.id), |
|
|
|
|
...yakBacks.map(y => y.id), |
|
|
|
|
...zapReceipts.map(z => z.id) |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
// Fetch zap receipts that reference thread or any comment/reply/yak/zap |
|
|
|
|
const zapFilters = [ |
|
|
|
|
{ |
|
|
|
|
kinds: [9735], |
|
|
|
|
'#e': Array.from(allEventIds) // Zap receipts for thread and all replies |
|
|
|
|
} |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
const zapEvents = await nostrClient.fetchEvents( |
|
|
|
|
zapFilters, |
|
|
|
|
allRelays, |
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const validZaps = zapEvents.filter(receipt => { |
|
|
|
|
// Filter by threshold |
|
|
|
|
const amountTag = receipt.tags.find((t) => t[0] === 'amount'); |
|
|
|
|
if (amountTag && amountTag[1]) { |
|
|
|
|
const amount = parseInt(amountTag[1], 10); |
|
|
|
|
return !isNaN(amount) && amount >= config.zapThreshold; |
|
|
|
|
} |
|
|
|
|
return false; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Add new zap receipts |
|
|
|
|
const existingZapIds = new Set(zapReceipts.map(z => z.id)); |
|
|
|
|
for (const zap of validZaps) { |
|
|
|
|
if (!existingZapIds.has(zap.id)) { |
|
|
|
|
zapReceipts.push(zap); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Check if any zaps reference events we don't have |
|
|
|
|
const missingEventIds = new Set<string>(); |
|
|
|
|
for (const zap of validZaps) { |
|
|
|
|
const eTag = zap.tags.find((t) => t[0] === 'e'); |
|
|
|
|
if (eTag && eTag[1] && eTag[1] !== threadId) { |
|
|
|
|
const exists = comments.some(c => c.id === eTag[1]) |
|
|
|
|
|| kind1Replies.some(r => r.id === eTag[1]) |
|
|
|
|
|| yakBacks.some(y => y.id === eTag[1]); |
|
|
|
|
if (!exists) { |
|
|
|
|
missingEventIds.add(eTag[1]); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fetch missing events (could be comments, replies, or yak backs) |
|
|
|
|
if (missingEventIds.size > 0) { |
|
|
|
|
const missingEvents = await nostrClient.fetchEvents( |
|
|
|
|
[ |
|
|
|
|
{ kinds: [1111], ids: Array.from(missingEventIds) }, |
|
|
|
|
{ kinds: [1], ids: Array.from(missingEventIds) }, |
|
|
|
|
{ kinds: [1244], ids: Array.from(missingEventIds) } |
|
|
|
|
], |
|
|
|
|
allRelays, |
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
for (const event of missingEvents) { |
|
|
|
|
if (event.kind === 1111 && !comments.some(c => c.id === event.id)) { |
|
|
|
|
comments.push(event); |
|
|
|
|
} else if (event.kind === 1 && !kind1Replies.some(r => r.id === event.id)) { |
|
|
|
|
kind1Replies.push(event); |
|
|
|
|
} else if (event.kind === 1244 && !yakBacks.some(y => y.id === event.id)) { |
|
|
|
|
yakBacks.push(event); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fetch nested replies to newly found events |
|
|
|
|
await fetchNestedReplies(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// Check if parent is the root event |
|
|
|
|
if (parentId === threadId) return event || undefined; |
|
|
|
|
|
|
|
|
|
// Find parent in loaded events |
|
|
|
|
return comments.find(c => c.id === parentId) |
|
|
|
|
|| kind1Replies.find(r => r.id === parentId) |
|
|
|
|
|| yakBacks.find(y => y.id === parentId) |
|
|
|
|
|| zapReceipts.find(z => z.id === parentId); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Sort thread items with proper nesting |
|
|
|
|
*/ |
|
|
|
|
function sortThreadItems(items: Array<{ event: NostrEvent; type: 'comment' | 'reply' | 'yak' | 'zap' }>): Array<{ event: NostrEvent; type: 'comment' | 'reply' | 'yak' | 'zap' }> { |
|
|
|
|
// Build thread structure similar to feed |
|
|
|
|
const eventMap = new Map<string, { event: NostrEvent; type: 'comment' | 'reply' | 'yak' | 'zap' }>(); |
|
|
|
|
const replyMap = new Map<string, string[]>(); // parentId -> childIds[] |
|
|
|
|
const rootItems: Array<{ event: NostrEvent; type: 'comment' | 'reply' | 'yak' | 'zap' }> = []; |
|
|
|
|
const allEventIds = new Set<string>(); |
|
|
|
|
|
|
|
|
|
// First pass: build event map and collect all event IDs |
|
|
|
|
// First pass: build event map |
|
|
|
|
for (const item of items) { |
|
|
|
|
eventMap.set(item.event.id, item); |
|
|
|
|
allEventIds.add(item.event.id); |
|
|
|
|
@ -254,24 +271,16 @@
@@ -254,24 +271,16 @@
|
|
|
|
|
|
|
|
|
|
// Second pass: determine parent-child relationships |
|
|
|
|
for (const item of items) { |
|
|
|
|
// Check if this is a reply - check both uppercase E (NIP-22) and lowercase e tags |
|
|
|
|
const eTag = item.event.tags.find((t) => t[0] === 'E') || item.event.tags.find((t) => t[0] === 'e' && t[1] !== item.event.id); |
|
|
|
|
const parentId = eTag?.[1]; |
|
|
|
|
const parentId = getParentEventId(item.event); |
|
|
|
|
|
|
|
|
|
if (parentId) { |
|
|
|
|
// Check if parent is the thread or another reply we have |
|
|
|
|
if (parentId === threadId || allEventIds.has(parentId)) { |
|
|
|
|
if (parentId && (parentId === threadId || allEventIds.has(parentId))) { |
|
|
|
|
// This is a reply |
|
|
|
|
if (!replyMap.has(parentId)) { |
|
|
|
|
replyMap.set(parentId, []); |
|
|
|
|
} |
|
|
|
|
replyMap.get(parentId)!.push(item.event.id); |
|
|
|
|
} else { |
|
|
|
|
// Parent not found - treat as root item (might be a missing parent) |
|
|
|
|
rootItems.push(item); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
// No parent tag - this is a root item (direct reply to thread) |
|
|
|
|
// No parent or parent not found - treat as root item |
|
|
|
|
rootItems.push(item); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -291,7 +300,7 @@
@@ -291,7 +300,7 @@
|
|
|
|
|
const replyItems = replies |
|
|
|
|
.map(id => eventMap.get(id)) |
|
|
|
|
.filter((item): item is { event: NostrEvent; type: 'comment' | 'reply' | 'yak' | 'zap' } => item !== undefined) |
|
|
|
|
.sort((a, b) => a.event.created_at - b.event.created_at); // Sort replies chronologically |
|
|
|
|
.sort((a, b) => a.event.created_at - b.event.created_at); |
|
|
|
|
|
|
|
|
|
for (const reply of replyItems) { |
|
|
|
|
addThread(reply); |
|
|
|
|
@ -317,31 +326,31 @@
@@ -317,31 +326,31 @@
|
|
|
|
|
return sortThreadItems(items); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getParentEvent(event: NostrEvent): NostrEvent | undefined { |
|
|
|
|
// NIP-22: E tag (uppercase) points to parent event, or lowercase e tag |
|
|
|
|
const eTag = event.tags.find((t) => t[0] === 'E') || event.tags.find((t) => t[0] === 'e' && t[1] !== event.id); |
|
|
|
|
if (eTag && eTag[1]) { |
|
|
|
|
// Find parent in comments, replies, yak backs, or zap receipts |
|
|
|
|
const parent = comments.find((c) => c.id === eTag[1]) |
|
|
|
|
|| kind1Replies.find((r) => r.id === eTag[1]) |
|
|
|
|
|| yakBacks.find((y) => y.id === eTag[1]) |
|
|
|
|
|| zapReceipts.find((z) => z.id === eTag[1]); |
|
|
|
|
if (parent) return parent; |
|
|
|
|
|
|
|
|
|
// If parent not found, it might be the thread itself |
|
|
|
|
return undefined; |
|
|
|
|
} |
|
|
|
|
return undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function handleReply(comment: NostrEvent) { |
|
|
|
|
replyingTo = comment; |
|
|
|
|
function handleReply(replyEvent: NostrEvent) { |
|
|
|
|
replyingTo = replyEvent; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function handleCommentPublished() { |
|
|
|
|
replyingTo = null; |
|
|
|
|
loadComments(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Determine what kind of reply is allowed for a given event |
|
|
|
|
*/ |
|
|
|
|
function getAllowedReplyKind(targetEvent: NostrEvent | null): number { |
|
|
|
|
if (!targetEvent) { |
|
|
|
|
// If replying to root, check root kind |
|
|
|
|
if (isKind1) return 1; |
|
|
|
|
return 1111; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// If target is kind 1, allow kind 1 reply |
|
|
|
|
if (targetEvent.kind === 1) return 1; |
|
|
|
|
|
|
|
|
|
// Everything else gets kind 1111 |
|
|
|
|
return 1111; |
|
|
|
|
} |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<div class="comment-thread"> |
|
|
|
|
@ -372,6 +381,7 @@
@@ -372,6 +381,7 @@
|
|
|
|
|
<FeedPost post={item.event} /> |
|
|
|
|
</div> |
|
|
|
|
{:else if item.type === 'zap'} |
|
|
|
|
<!-- Zap receipt - render with lightning bolt --> |
|
|
|
|
<ZapReceiptReply |
|
|
|
|
zapReceipt={item.event} |
|
|
|
|
parentEvent={parent} |
|
|
|
|
@ -386,6 +396,7 @@
@@ -386,6 +396,7 @@
|
|
|
|
|
<div class="reply-form-container mt-4"> |
|
|
|
|
<CommentForm |
|
|
|
|
threadId={threadId} |
|
|
|
|
rootEvent={event} |
|
|
|
|
parentEvent={replyingTo} |
|
|
|
|
onPublished={handleCommentPublished} |
|
|
|
|
onCancel={() => (replyingTo = null)} |
|
|
|
|
@ -394,7 +405,8 @@
@@ -394,7 +405,8 @@
|
|
|
|
|
{:else} |
|
|
|
|
<div class="new-comment-container mt-4"> |
|
|
|
|
<CommentForm |
|
|
|
|
{threadId} |
|
|
|
|
threadId={threadId} |
|
|
|
|
rootEvent={event} |
|
|
|
|
onPublished={handleCommentPublished} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
|