|
|
|
@ -45,11 +45,8 @@ |
|
|
|
let { singleRelay }: Props = $props(); |
|
|
|
let { singleRelay }: Props = $props(); |
|
|
|
|
|
|
|
|
|
|
|
let posts = $state<NostrEvent[]>([]); |
|
|
|
let posts = $state<NostrEvent[]>([]); |
|
|
|
let allPosts = $state<NostrEvent[]>([]); // Store all posts before filtering |
|
|
|
|
|
|
|
let highlights = $state<NostrEvent[]>([]); // Store highlight events (kind 9802) |
|
|
|
let highlights = $state<NostrEvent[]>([]); // Store highlight events (kind 9802) |
|
|
|
let allHighlights = $state<NostrEvent[]>([]); // Store all highlights before filtering |
|
|
|
|
|
|
|
let otherFeedEvents = $state<NostrEvent[]>([]); // Store other feed kinds (not kind 1 or 9802) |
|
|
|
let otherFeedEvents = $state<NostrEvent[]>([]); // Store other feed kinds (not kind 1 or 9802) |
|
|
|
let allOtherFeedEvents = $state<NostrEvent[]>([]); // Store all other feed events before filtering |
|
|
|
|
|
|
|
let loading = $state(true); |
|
|
|
let loading = $state(true); |
|
|
|
let loadingMore = $state(false); |
|
|
|
let loadingMore = $state(false); |
|
|
|
let hasMore = $state(true); |
|
|
|
let hasMore = $state(true); |
|
|
|
@ -65,11 +62,6 @@ |
|
|
|
const allEvents = $derived.by(() => [...posts, ...highlights, ...otherFeedEvents].sort((a, b) => b.created_at - a.created_at)); |
|
|
|
const allEvents = $derived.by(() => [...posts, ...highlights, ...otherFeedEvents].sort((a, b) => b.created_at - a.created_at)); |
|
|
|
const visibleEvents = $derived.by(() => allEvents.slice(0, visibleItemCount)); |
|
|
|
const visibleEvents = $derived.by(() => allEvents.slice(0, visibleItemCount)); |
|
|
|
|
|
|
|
|
|
|
|
// List filter state |
|
|
|
|
|
|
|
let availableLists = $state<Array<{ kind: number; name: string; event: NostrEvent }>>([]); |
|
|
|
|
|
|
|
let selectedListId = $state<string | null>(null); // Format: "kind:eventId" |
|
|
|
|
|
|
|
let listFilterIds = $state<Set<string>>(new Set()); // Event IDs or pubkeys to filter by |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Batch-loaded parent events: eventId -> parentEvent |
|
|
|
// Batch-loaded parent events: eventId -> parentEvent |
|
|
|
let parentEventsMap = $state<Map<string, NostrEvent>>(new Map()); |
|
|
|
let parentEventsMap = $state<Map<string, NostrEvent>>(new Map()); |
|
|
|
|
|
|
|
|
|
|
|
@ -109,8 +101,6 @@ |
|
|
|
isMounted = true; |
|
|
|
isMounted = true; |
|
|
|
await nostrClient.initialize(); |
|
|
|
await nostrClient.initialize(); |
|
|
|
if (!isMounted) return; // Check if unmounted during init |
|
|
|
if (!isMounted) return; // Check if unmounted during init |
|
|
|
await loadUserLists(); |
|
|
|
|
|
|
|
if (!isMounted) return; |
|
|
|
|
|
|
|
await loadFeed(); |
|
|
|
await loadFeed(); |
|
|
|
if (!isMounted) return; |
|
|
|
if (!isMounted) return; |
|
|
|
// Set up persistent subscription for new events (only once) |
|
|
|
// Set up persistent subscription for new events (only once) |
|
|
|
@ -121,144 +111,8 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Load user lists for filtering |
|
|
|
// Reset visible count when data changes (new feed loaded) |
|
|
|
async function loadUserLists() { |
|
|
|
|
|
|
|
// Don't load user lists for single relay mode |
|
|
|
|
|
|
|
if (singleRelay) { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const session = sessionManager.getSession(); |
|
|
|
|
|
|
|
if (!session) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const listKinds = [ |
|
|
|
|
|
|
|
KIND.CONTACTS, |
|
|
|
|
|
|
|
KIND.FAVORITE_RELAYS, |
|
|
|
|
|
|
|
KIND.RELAY_LIST, |
|
|
|
|
|
|
|
KIND.LOCAL_RELAYS, |
|
|
|
|
|
|
|
KIND.PIN_LIST, |
|
|
|
|
|
|
|
KIND.BOOKMARKS, |
|
|
|
|
|
|
|
KIND.INTEREST_LIST, |
|
|
|
|
|
|
|
KIND.FOLOW_SET |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const relays = relayManager.getProfileReadRelays(); |
|
|
|
|
|
|
|
const lists: Array<{ kind: number; name: string; event: NostrEvent }> = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch all list types |
|
|
|
|
|
|
|
for (const kind of listKinds) { |
|
|
|
|
|
|
|
const limit = kind === KIND.FOLOW_SET ? 50 : 1; // Multiple follow sets allowed |
|
|
|
|
|
|
|
const events = await nostrClient.fetchEvents( |
|
|
|
|
|
|
|
[{ kinds: [kind], authors: [session.pubkey], limit }], |
|
|
|
|
|
|
|
relays, |
|
|
|
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const kindName = getKindName(kind); |
|
|
|
|
|
|
|
for (const event of events) { |
|
|
|
|
|
|
|
lists.push({ |
|
|
|
|
|
|
|
kind, |
|
|
|
|
|
|
|
name: `${kindName}${kind === KIND.FOLOW_SET ? ` (${new Date(event.created_at * 1000).toLocaleDateString()})` : ''}`, |
|
|
|
|
|
|
|
event |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
availableLists = lists; |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
console.error('Error loading user lists:', error); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getKindName(kind: number): string { |
|
|
|
|
|
|
|
const names: Record<number, string> = { |
|
|
|
|
|
|
|
[KIND.CONTACTS]: 'Contacts', |
|
|
|
|
|
|
|
[KIND.FAVORITE_RELAYS]: 'Favorite Relays', |
|
|
|
|
|
|
|
[KIND.RELAY_LIST]: 'Relay List', |
|
|
|
|
|
|
|
[KIND.LOCAL_RELAYS]: 'Local Relays', |
|
|
|
|
|
|
|
[KIND.PIN_LIST]: 'Pin List', |
|
|
|
|
|
|
|
[KIND.BOOKMARKS]: 'Bookmarks', |
|
|
|
|
|
|
|
[KIND.INTEREST_LIST]: 'Interest List', |
|
|
|
|
|
|
|
[KIND.FOLOW_SET]: 'Follow Set' |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
return names[kind] || `Kind ${kind}`; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleListFilterChange(listId: string | null) { |
|
|
|
|
|
|
|
selectedListId = listId; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!listId) { |
|
|
|
|
|
|
|
// No filter selected - show all posts, highlights, and other feed events |
|
|
|
|
|
|
|
listFilterIds = new Set(); |
|
|
|
|
|
|
|
posts = [...allPosts]; |
|
|
|
|
|
|
|
highlights = [...allHighlights]; |
|
|
|
|
|
|
|
otherFeedEvents = [...allOtherFeedEvents]; |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Find the selected list |
|
|
|
|
|
|
|
const [kindStr, eventId] = listId.split(':'); |
|
|
|
|
|
|
|
const kind = parseInt(kindStr, 10); |
|
|
|
|
|
|
|
const list = availableLists.find(l => l.kind === kind && l.event.id === eventId); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!list) { |
|
|
|
|
|
|
|
listFilterIds = new Set(); |
|
|
|
|
|
|
|
posts = [...allPosts]; |
|
|
|
|
|
|
|
highlights = [...allHighlights]; |
|
|
|
|
|
|
|
otherFeedEvents = [...allOtherFeedEvents]; |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Extract IDs from the list |
|
|
|
|
|
|
|
const ids = new Set<string>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// For contacts and follow sets, extract pubkeys from 'p' tags |
|
|
|
|
|
|
|
if (kind === KIND.CONTACTS || kind === KIND.FOLOW_SET) { |
|
|
|
|
|
|
|
for (const tag of list.event.tags) { |
|
|
|
|
|
|
|
if (tag[0] === 'p' && tag[1]) { |
|
|
|
|
|
|
|
ids.add(tag[1]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// For other lists, extract event IDs from 'e' and 'a' tags |
|
|
|
|
|
|
|
for (const tag of list.event.tags) { |
|
|
|
|
|
|
|
if (tag[0] === 'e' && tag[1]) { |
|
|
|
|
|
|
|
ids.add(tag[1]); |
|
|
|
|
|
|
|
} else if (tag[0] === 'a' && tag[1]) { |
|
|
|
|
|
|
|
// For 'a' tags, we'd need to resolve them to event IDs |
|
|
|
|
|
|
|
// For now, we'll just use the 'a' tag value as-is |
|
|
|
|
|
|
|
// This is a simplified approach - full implementation would resolve 'a' tags |
|
|
|
|
|
|
|
ids.add(tag[1]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
listFilterIds = ids; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Filter posts, highlights, and other feed events |
|
|
|
|
|
|
|
if (kind === KIND.CONTACTS || kind === KIND.FOLOW_SET) { |
|
|
|
|
|
|
|
// Filter by author pubkey |
|
|
|
|
|
|
|
posts = allPosts.filter(post => ids.has(post.pubkey)); |
|
|
|
|
|
|
|
highlights = allHighlights.filter(highlight => ids.has(highlight.pubkey)); |
|
|
|
|
|
|
|
otherFeedEvents = allOtherFeedEvents.filter(event => ids.has(event.pubkey)); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Filter by event ID |
|
|
|
|
|
|
|
posts = allPosts.filter(post => ids.has(post.id)); |
|
|
|
|
|
|
|
highlights = allHighlights.filter(highlight => ids.has(highlight.id)); |
|
|
|
|
|
|
|
otherFeedEvents = allOtherFeedEvents.filter((event: NostrEvent) => ids.has(event.id)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Apply filter when allPosts, allHighlights, or allOtherFeedEvents changes |
|
|
|
|
|
|
|
$effect(() => { |
|
|
|
$effect(() => { |
|
|
|
if (selectedListId) { |
|
|
|
|
|
|
|
handleListFilterChange(selectedListId); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
posts = [...allPosts]; |
|
|
|
|
|
|
|
highlights = [...allHighlights]; |
|
|
|
|
|
|
|
otherFeedEvents = [...allOtherFeedEvents]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Reset visible count when data changes (new feed loaded) |
|
|
|
// Reset visible count when data changes (new feed loaded) |
|
|
|
visibleItemCount = INITIAL_RENDER_LIMIT; |
|
|
|
visibleItemCount = INITIAL_RENDER_LIMIT; |
|
|
|
}); |
|
|
|
}); |
|
|
|
@ -554,16 +408,14 @@ |
|
|
|
getKindInfo(e.kind).showInFeed === true |
|
|
|
getKindInfo(e.kind).showInFeed === true |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Sort by created_at descending and deduplicate |
|
|
|
// Deduplicate (relays already return events in reverse chronological order) |
|
|
|
const uniquePostsMap = new Map<string, NostrEvent>(); |
|
|
|
const uniquePostsMap = new Map<string, NostrEvent>(); |
|
|
|
for (const event of postsList) { |
|
|
|
for (const event of postsList) { |
|
|
|
if (!uniquePostsMap.has(event.id)) { |
|
|
|
if (!uniquePostsMap.has(event.id)) { |
|
|
|
uniquePostsMap.set(event.id, event); |
|
|
|
uniquePostsMap.set(event.id, event); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
const uniquePosts = Array.from(uniquePostsMap.values()); |
|
|
|
posts = Array.from(uniquePostsMap.values()); |
|
|
|
const sortedPosts = uniquePosts.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
|
|
|
allPosts = sortedPosts; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const uniqueHighlightsMap = new Map<string, NostrEvent>(); |
|
|
|
const uniqueHighlightsMap = new Map<string, NostrEvent>(); |
|
|
|
for (const event of highlightsList) { |
|
|
|
for (const event of highlightsList) { |
|
|
|
@ -571,9 +423,7 @@ |
|
|
|
uniqueHighlightsMap.set(event.id, event); |
|
|
|
uniqueHighlightsMap.set(event.id, event); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
const uniqueHighlights = Array.from(uniqueHighlightsMap.values()); |
|
|
|
highlights = Array.from(uniqueHighlightsMap.values()); |
|
|
|
const sortedHighlights = uniqueHighlights.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
|
|
|
allHighlights = sortedHighlights; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Store other feed events |
|
|
|
// Store other feed events |
|
|
|
const uniqueOtherMap = new Map<string, NostrEvent>(); |
|
|
|
const uniqueOtherMap = new Map<string, NostrEvent>(); |
|
|
|
@ -582,42 +432,29 @@ |
|
|
|
uniqueOtherMap.set(event.id, event); |
|
|
|
uniqueOtherMap.set(event.id, event); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
const uniqueOther = Array.from(uniqueOtherMap.values()); |
|
|
|
otherFeedEvents = Array.from(uniqueOtherMap.values()); |
|
|
|
const sortedOther = uniqueOther.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
|
|
|
allOtherFeedEvents = sortedOther; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Always set posts, highlights, and other feed events immediately, even if empty |
|
|
|
|
|
|
|
// This ensures cached data shows up right away |
|
|
|
|
|
|
|
// Apply filter if one is selected |
|
|
|
|
|
|
|
if (selectedListId) { |
|
|
|
|
|
|
|
handleListFilterChange(selectedListId); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
posts = [...allPosts]; |
|
|
|
|
|
|
|
highlights = [...allHighlights]; |
|
|
|
|
|
|
|
otherFeedEvents = [...allOtherFeedEvents]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Set loading to false immediately after showing cached data |
|
|
|
// Set loading to false immediately after showing cached data |
|
|
|
// This allows the UI to render while fresh data loads in background |
|
|
|
// This allows the UI to render while fresh data loads in background |
|
|
|
loading = false; |
|
|
|
loading = false; |
|
|
|
|
|
|
|
|
|
|
|
console.log(`[FeedPage] Loaded ${sortedPosts.length} posts and ${sortedHighlights.length} highlights`); |
|
|
|
console.log(`[FeedPage] Loaded ${posts.length} posts and ${highlights.length} highlights`); |
|
|
|
|
|
|
|
|
|
|
|
if (sortedPosts.length > 0 || sortedHighlights.length > 0) { |
|
|
|
if (posts.length > 0 || highlights.length > 0) { |
|
|
|
const allTimestamps = [...sortedPosts.map(e => e.created_at), ...sortedHighlights.map(e => e.created_at)]; |
|
|
|
const allTimestamps = [...posts.map(e => e.created_at), ...highlights.map(e => e.created_at)]; |
|
|
|
oldestTimestamp = Math.min(...allTimestamps); |
|
|
|
oldestTimestamp = Math.min(...allTimestamps); |
|
|
|
|
|
|
|
|
|
|
|
// Load secondary data (reactions, profiles, etc.) AFTER posts are displayed |
|
|
|
// Load secondary data (reactions, profiles, etc.) AFTER posts are displayed |
|
|
|
// Collect all post IDs and pubkeys first, then batch fetch everything |
|
|
|
// Collect all post IDs and pubkeys first, then batch fetch everything |
|
|
|
const allPostIds = [ |
|
|
|
const allPostIds = [ |
|
|
|
...sortedPosts.map(p => p.id), |
|
|
|
...posts.map(p => p.id), |
|
|
|
...sortedHighlights.map(p => p.id), |
|
|
|
...highlights.map(p => p.id), |
|
|
|
...sortedOther.map(p => p.id) |
|
|
|
...otherFeedEvents.map(p => p.id) |
|
|
|
]; |
|
|
|
]; |
|
|
|
const allPubkeys = new Set<string>(); |
|
|
|
const allPubkeys = new Set<string>(); |
|
|
|
sortedPosts.forEach(p => allPubkeys.add(p.pubkey)); |
|
|
|
posts.forEach(p => allPubkeys.add(p.pubkey)); |
|
|
|
sortedHighlights.forEach(p => allPubkeys.add(p.pubkey)); |
|
|
|
highlights.forEach(p => allPubkeys.add(p.pubkey)); |
|
|
|
sortedOther.forEach(p => allPubkeys.add(p.pubkey)); |
|
|
|
otherFeedEvents.forEach(p => allPubkeys.add(p.pubkey)); |
|
|
|
|
|
|
|
|
|
|
|
// Use requestIdleCallback or setTimeout to defer loading so posts render first |
|
|
|
// Use requestIdleCallback or setTimeout to defer loading so posts render first |
|
|
|
const deferSecondaryData = () => { |
|
|
|
const deferSecondaryData = () => { |
|
|
|
@ -626,9 +463,9 @@ |
|
|
|
// Batch load all secondary data in parallel using collected IDs/pubkeys |
|
|
|
// Batch load all secondary data in parallel using collected IDs/pubkeys |
|
|
|
// Note: Reactions are handled by FeedPost component itself |
|
|
|
// Note: Reactions are handled by FeedPost component itself |
|
|
|
const promise = Promise.all([ |
|
|
|
const promise = Promise.all([ |
|
|
|
loadParentAndQuotedEvents(sortedPosts), |
|
|
|
loadParentAndQuotedEvents(posts), |
|
|
|
loadZapCountsForPosts(sortedPosts), |
|
|
|
loadZapCountsForPosts(posts), |
|
|
|
loadProfilesForPosts(sortedPosts) |
|
|
|
loadProfilesForPosts(posts) |
|
|
|
]).catch(error => { |
|
|
|
]).catch(error => { |
|
|
|
if (isMounted) { |
|
|
|
if (isMounted) { |
|
|
|
console.error('[FeedPage] Error loading secondary data:', error); |
|
|
|
console.error('[FeedPage] Error loading secondary data:', error); |
|
|
|
@ -716,17 +553,17 @@ |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Filter out duplicates |
|
|
|
// Filter out duplicates |
|
|
|
const existingPostIds = new Set(allPosts.map(p => p.id)); |
|
|
|
const existingPostIds = new Set(posts.map(p => p.id)); |
|
|
|
const existingHighlightIds = new Set(allHighlights.map(h => h.id)); |
|
|
|
const existingHighlightIds = new Set(highlights.map(h => h.id)); |
|
|
|
const existingOtherIds = new Set(allOtherFeedEvents.map((e: NostrEvent) => e.id)); |
|
|
|
const existingOtherIds = new Set(otherFeedEvents.map((e: NostrEvent) => e.id)); |
|
|
|
const uniqueNewPosts = newPosts.filter(e => !existingPostIds.has(e.id)); |
|
|
|
const uniqueNewPosts = newPosts.filter(e => !existingPostIds.has(e.id)); |
|
|
|
const uniqueNewHighlights = newHighlights.filter(e => !existingHighlightIds.has(e.id)); |
|
|
|
const uniqueNewHighlights = newHighlights.filter(e => !existingHighlightIds.has(e.id)); |
|
|
|
const uniqueNewOther = newOtherFeedEvents.filter((e: NostrEvent) => !existingOtherIds.has(e.id)); |
|
|
|
const uniqueNewOther = newOtherFeedEvents.filter((e: NostrEvent) => !existingOtherIds.has(e.id)); |
|
|
|
|
|
|
|
|
|
|
|
if (uniqueNewPosts.length > 0 || uniqueNewHighlights.length > 0) { |
|
|
|
if (uniqueNewPosts.length > 0 || uniqueNewHighlights.length > 0) { |
|
|
|
if (uniqueNewPosts.length > 0) { |
|
|
|
if (uniqueNewPosts.length > 0) { |
|
|
|
const sorted = uniqueNewPosts.sort((a, b) => b.created_at - a.created_at); |
|
|
|
// Append new posts (they're already in reverse chronological order from relays) |
|
|
|
allPosts = [...allPosts, ...sorted]; |
|
|
|
posts = [...posts, ...uniqueNewPosts]; |
|
|
|
// Load secondary data with low priority after posts are displayed |
|
|
|
// Load secondary data with low priority after posts are displayed |
|
|
|
const secondaryDataPromise = new Promise<void>((resolve) => { |
|
|
|
const secondaryDataPromise = new Promise<void>((resolve) => { |
|
|
|
setTimeout(() => { |
|
|
|
setTimeout(() => { |
|
|
|
@ -736,9 +573,9 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
// Note: Reactions are handled by FeedPost component itself |
|
|
|
// Note: Reactions are handled by FeedPost component itself |
|
|
|
const promise = Promise.all([ |
|
|
|
const promise = Promise.all([ |
|
|
|
loadParentAndQuotedEvents(sorted), |
|
|
|
loadParentAndQuotedEvents(uniqueNewPosts), |
|
|
|
loadZapCountsForPosts(sorted), |
|
|
|
loadZapCountsForPosts(uniqueNewPosts), |
|
|
|
loadProfilesForPosts(sorted) |
|
|
|
loadProfilesForPosts(uniqueNewPosts) |
|
|
|
]).catch(error => { |
|
|
|
]).catch(error => { |
|
|
|
if (isMounted) { // Only log if still mounted |
|
|
|
if (isMounted) { // Only log if still mounted |
|
|
|
console.error('[FeedPage] Error loading secondary data for new posts:', error); |
|
|
|
console.error('[FeedPage] Error loading secondary data for new posts:', error); |
|
|
|
@ -754,23 +591,13 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (uniqueNewHighlights.length > 0) { |
|
|
|
if (uniqueNewHighlights.length > 0) { |
|
|
|
const sorted = uniqueNewHighlights.sort((a, b) => b.created_at - a.created_at); |
|
|
|
// Append new highlights (they're already in reverse chronological order from relays) |
|
|
|
allHighlights = [...allHighlights, ...sorted]; |
|
|
|
highlights = [...highlights, ...uniqueNewHighlights]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (uniqueNewOther.length > 0) { |
|
|
|
if (uniqueNewOther.length > 0) { |
|
|
|
const sorted = uniqueNewOther.sort((a, b) => b.created_at - a.created_at); |
|
|
|
// Append new other events (they're already in reverse chronological order from relays) |
|
|
|
allOtherFeedEvents = [...allOtherFeedEvents, ...sorted]; |
|
|
|
otherFeedEvents = [...otherFeedEvents, ...uniqueNewOther]; |
|
|
|
otherFeedEvents = [...allOtherFeedEvents]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Apply filter if one is selected |
|
|
|
|
|
|
|
if (selectedListId) { |
|
|
|
|
|
|
|
handleListFilterChange(selectedListId); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
posts = [...allPosts]; |
|
|
|
|
|
|
|
highlights = [...allHighlights]; |
|
|
|
|
|
|
|
otherFeedEvents = [...allOtherFeedEvents]; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const allNewTimestamps = [...uniqueNewPosts.map(e => e.created_at), ...uniqueNewHighlights.map(e => e.created_at)]; |
|
|
|
const allNewTimestamps = [...uniqueNewPosts.map(e => e.created_at), ...uniqueNewHighlights.map(e => e.created_at)]; |
|
|
|
@ -808,9 +635,9 @@ |
|
|
|
// Deduplicate incoming updates before adding to pending |
|
|
|
// Deduplicate incoming updates before adding to pending |
|
|
|
// Check against all feed event types |
|
|
|
// Check against all feed event types |
|
|
|
const existingIds = new Set([ |
|
|
|
const existingIds = new Set([ |
|
|
|
...allPosts.map(p => p.id), |
|
|
|
...posts.map((p: NostrEvent) => p.id), |
|
|
|
...allHighlights.map(h => h.id), |
|
|
|
...highlights.map((h: NostrEvent) => h.id), |
|
|
|
...allOtherFeedEvents.map(e => e.id) |
|
|
|
...otherFeedEvents.map((e: NostrEvent) => e.id) |
|
|
|
]); |
|
|
|
]); |
|
|
|
const newUpdates = updated.filter(e => e && e.id && !existingIds.has(e.id)); |
|
|
|
const newUpdates = updated.filter(e => e && e.id && !existingIds.has(e.id)); |
|
|
|
|
|
|
|
|
|
|
|
@ -841,9 +668,9 @@ |
|
|
|
|
|
|
|
|
|
|
|
// Final deduplication check against all feed event types (may have changed) |
|
|
|
// Final deduplication check against all feed event types (may have changed) |
|
|
|
const currentIds = new Set([ |
|
|
|
const currentIds = new Set([ |
|
|
|
...allPosts.map(p => p.id), |
|
|
|
...posts.map(p => p.id), |
|
|
|
...allHighlights.map(h => h.id), |
|
|
|
...highlights.map(h => h.id), |
|
|
|
...allOtherFeedEvents.map(e => e.id) |
|
|
|
...otherFeedEvents.map(e => e.id) |
|
|
|
]); |
|
|
|
]); |
|
|
|
const newEvents = pendingUpdates.filter(e => e && e.id && !currentIds.has(e.id)); |
|
|
|
const newEvents = pendingUpdates.filter(e => e && e.id && !currentIds.has(e.id)); |
|
|
|
|
|
|
|
|
|
|
|
@ -852,7 +679,7 @@ |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log(`[FeedPage] Processing ${newEvents.length} new events, existing: ${allPosts.length}`); |
|
|
|
console.log(`[FeedPage] Processing ${newEvents.length} new events, existing: ${posts.length}`); |
|
|
|
|
|
|
|
|
|
|
|
// Separate events by kind |
|
|
|
// Separate events by kind |
|
|
|
const newPosts = newEvents.filter(e => e.kind === KIND.SHORT_TEXT_NOTE); |
|
|
|
const newPosts = newEvents.filter(e => e.kind === KIND.SHORT_TEXT_NOTE); |
|
|
|
@ -863,9 +690,9 @@ |
|
|
|
getKindInfo(e.kind).showInFeed === true |
|
|
|
getKindInfo(e.kind).showInFeed === true |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Merge and sort posts, then deduplicate by ID |
|
|
|
// Merge and deduplicate posts (relays already return in reverse chronological order) |
|
|
|
if (newPosts.length > 0) { |
|
|
|
if (newPosts.length > 0) { |
|
|
|
const mergedPosts = [...allPosts, ...newPosts]; |
|
|
|
const mergedPosts = [...posts, ...newPosts]; |
|
|
|
const uniquePostsMap = new Map<string, NostrEvent>(); |
|
|
|
const uniquePostsMap = new Map<string, NostrEvent>(); |
|
|
|
for (const event of mergedPosts) { |
|
|
|
for (const event of mergedPosts) { |
|
|
|
if (event && event.id && !uniquePostsMap.has(event.id)) { |
|
|
|
if (event && event.id && !uniquePostsMap.has(event.id)) { |
|
|
|
@ -873,17 +700,16 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
const uniquePosts = Array.from(uniquePostsMap.values()); |
|
|
|
const uniquePosts = Array.from(uniquePostsMap.values()); |
|
|
|
const sortedPosts = uniquePosts.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Only update if we actually have new events to prevent loops |
|
|
|
// Only update if we actually have new events to prevent loops |
|
|
|
if (sortedPosts.length > allPosts.length || sortedPosts.some((e, i) => e.id !== allPosts[i]?.id)) { |
|
|
|
if (uniquePosts.length > posts.length || uniquePosts.some((e, i) => e.id !== posts[i]?.id)) { |
|
|
|
allPosts = sortedPosts; |
|
|
|
posts = uniquePosts; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Merge and sort highlights, then deduplicate by ID |
|
|
|
// Merge and deduplicate highlights (relays already return in reverse chronological order) |
|
|
|
if (newHighlights.length > 0) { |
|
|
|
if (newHighlights.length > 0) { |
|
|
|
const mergedHighlights = [...allHighlights, ...newHighlights]; |
|
|
|
const mergedHighlights = [...highlights, ...newHighlights]; |
|
|
|
const uniqueHighlightsMap = new Map<string, NostrEvent>(); |
|
|
|
const uniqueHighlightsMap = new Map<string, NostrEvent>(); |
|
|
|
for (const event of mergedHighlights) { |
|
|
|
for (const event of mergedHighlights) { |
|
|
|
if (event && event.id && !uniqueHighlightsMap.has(event.id)) { |
|
|
|
if (event && event.id && !uniqueHighlightsMap.has(event.id)) { |
|
|
|
@ -891,17 +717,16 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
const uniqueHighlights = Array.from(uniqueHighlightsMap.values()); |
|
|
|
const uniqueHighlights = Array.from(uniqueHighlightsMap.values()); |
|
|
|
const sortedHighlights = uniqueHighlights.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Only update if we actually have new events to prevent loops |
|
|
|
// Only update if we actually have new events to prevent loops |
|
|
|
if (sortedHighlights.length > allHighlights.length || sortedHighlights.some((e, i) => e.id !== allHighlights[i]?.id)) { |
|
|
|
if (uniqueHighlights.length > highlights.length || uniqueHighlights.some((e, i) => e.id !== highlights[i]?.id)) { |
|
|
|
allHighlights = sortedHighlights; |
|
|
|
highlights = uniqueHighlights; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Merge and sort other feed events, then deduplicate by ID |
|
|
|
// Merge and deduplicate other feed events (relays already return in reverse chronological order) |
|
|
|
if (newOtherFeedEvents.length > 0) { |
|
|
|
if (newOtherFeedEvents.length > 0) { |
|
|
|
const mergedOther = [...allOtherFeedEvents, ...newOtherFeedEvents]; |
|
|
|
const mergedOther = [...otherFeedEvents, ...newOtherFeedEvents]; |
|
|
|
const uniqueOtherMap = new Map<string, NostrEvent>(); |
|
|
|
const uniqueOtherMap = new Map<string, NostrEvent>(); |
|
|
|
for (const event of mergedOther) { |
|
|
|
for (const event of mergedOther) { |
|
|
|
if (event && event.id && !uniqueOtherMap.has(event.id)) { |
|
|
|
if (event && event.id && !uniqueOtherMap.has(event.id)) { |
|
|
|
@ -909,24 +734,14 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
const uniqueOther = Array.from(uniqueOtherMap.values()); |
|
|
|
const uniqueOther = Array.from(uniqueOtherMap.values()); |
|
|
|
const sortedOther = uniqueOther.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Only update if we actually have new events to prevent loops |
|
|
|
// Only update if we actually have new events to prevent loops |
|
|
|
if (sortedOther.length > allOtherFeedEvents.length || sortedOther.some((e, i) => e.id !== allOtherFeedEvents[i]?.id)) { |
|
|
|
if (uniqueOther.length > otherFeedEvents.length || uniqueOther.some((e, i) => e.id !== otherFeedEvents[i]?.id)) { |
|
|
|
allOtherFeedEvents = sortedOther; |
|
|
|
otherFeedEvents = uniqueOther; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Apply filter if one is selected |
|
|
|
console.debug(`[FeedPage] Updated: ${posts.length} posts, ${highlights.length} highlights`); |
|
|
|
if (selectedListId) { |
|
|
|
|
|
|
|
handleListFilterChange(selectedListId); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
posts = [...allPosts]; |
|
|
|
|
|
|
|
highlights = [...allHighlights]; |
|
|
|
|
|
|
|
otherFeedEvents = [...allOtherFeedEvents]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.debug(`[FeedPage] Updated: ${allPosts.length} posts, ${allHighlights.length} highlights`); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pendingUpdates = []; |
|
|
|
pendingUpdates = []; |
|
|
|
}, 500); |
|
|
|
}, 500); |
|
|
|
@ -1127,23 +942,6 @@ |
|
|
|
</script> |
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
<div class="feed-page"> |
|
|
|
<div class="feed-page"> |
|
|
|
{#if !loading && availableLists.length > 0 && !singleRelay} |
|
|
|
|
|
|
|
<div class="feed-filter"> |
|
|
|
|
|
|
|
<label for="list-filter" class="filter-label">Filter by list:</label> |
|
|
|
|
|
|
|
<select |
|
|
|
|
|
|
|
id="list-filter" |
|
|
|
|
|
|
|
bind:value={selectedListId} |
|
|
|
|
|
|
|
onchange={(e) => handleListFilterChange((e.target as HTMLSelectElement).value || null)} |
|
|
|
|
|
|
|
class="filter-select" |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<option value="">All Posts</option> |
|
|
|
|
|
|
|
{#each availableLists as list} |
|
|
|
|
|
|
|
<option value="{list.kind}:{list.event.id}">{list.name}</option> |
|
|
|
|
|
|
|
{/each} |
|
|
|
|
|
|
|
</select> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{#if singleRelay} |
|
|
|
{#if singleRelay} |
|
|
|
<div class="relay-info"> |
|
|
|
<div class="relay-info"> |
|
|
|
<p class="relay-info-text"> |
|
|
|
<p class="relay-info-text"> |
|
|
|
@ -1166,11 +964,7 @@ |
|
|
|
{:else if posts.length === 0 && highlights.length === 0 && otherFeedEvents.length === 0} |
|
|
|
{:else if posts.length === 0 && highlights.length === 0 && otherFeedEvents.length === 0} |
|
|
|
<div class="empty-state"> |
|
|
|
<div class="empty-state"> |
|
|
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
|
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
|
|
{#if selectedListId} |
|
|
|
No posts found. Check back later! |
|
|
|
No posts found in selected list. |
|
|
|
|
|
|
|
{:else} |
|
|
|
|
|
|
|
No posts found. Check back later! |
|
|
|
|
|
|
|
{/if} |
|
|
|
|
|
|
|
</p> |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{:else} |
|
|
|
{:else} |
|
|
|
@ -1225,51 +1019,6 @@ |
|
|
|
max-width: 100%; |
|
|
|
max-width: 100%; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.feed-filter { |
|
|
|
|
|
|
|
margin-bottom: 1.5rem; |
|
|
|
|
|
|
|
display: flex; |
|
|
|
|
|
|
|
align-items: center; |
|
|
|
|
|
|
|
gap: 0.75rem; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.filter-label { |
|
|
|
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
|
|
|
font-weight: 500; |
|
|
|
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:global(.dark) .filter-label { |
|
|
|
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.filter-select { |
|
|
|
|
|
|
|
padding: 0.5rem 0.75rem; |
|
|
|
|
|
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
|
|
|
border-radius: 0.375rem; |
|
|
|
|
|
|
|
background: var(--fog-post, #ffffff); |
|
|
|
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
|
|
min-width: 200px; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.filter-select:focus { |
|
|
|
|
|
|
|
outline: none; |
|
|
|
|
|
|
|
border-color: var(--fog-accent, #64748b); |
|
|
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.1); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:global(.dark) .filter-select { |
|
|
|
|
|
|
|
background: var(--fog-dark-post, #1f2937); |
|
|
|
|
|
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
|
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:global(.dark) .filter-select:focus { |
|
|
|
|
|
|
|
border-color: var(--fog-dark-accent, #94a3b8); |
|
|
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.1); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.loading-state, |
|
|
|
.loading-state, |
|
|
|
.empty-state, |
|
|
|
.empty-state, |
|
|
|
.error-state { |
|
|
|
.error-state { |
|
|
|
|