|
|
|
|
@ -10,29 +10,30 @@
@@ -10,29 +10,30 @@
|
|
|
|
|
import { getRecentFeedEvents } from '../../services/cache/event-cache.js'; |
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
|
singleRelay?: string; // If provided, use only this relay and disable cache |
|
|
|
|
singleRelay?: string; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let { singleRelay }: Props = $props(); |
|
|
|
|
|
|
|
|
|
// Core state |
|
|
|
|
let events = $state<NostrEvent[]>([]); |
|
|
|
|
let loading = $state(true); |
|
|
|
|
let relayError = $state<string | null>(null); |
|
|
|
|
|
|
|
|
|
// Batch-loaded parent and quoted events |
|
|
|
|
let parentEventsMap = $state<Map<string, NostrEvent>>(new Map()); |
|
|
|
|
let quotedEventsMap = $state<Map<string, NostrEvent>>(new Map()); |
|
|
|
|
|
|
|
|
|
// Drawer state |
|
|
|
|
let drawerOpen = $state(false); |
|
|
|
|
let drawerEvent = $state<NostrEvent | null>(null); |
|
|
|
|
|
|
|
|
|
// Waiting room for new events |
|
|
|
|
let waitingRoomEvents = $state<NostrEvent[]>([]); |
|
|
|
|
|
|
|
|
|
// Pagination |
|
|
|
|
let oldestTimestamp = $state<number | null>(null); |
|
|
|
|
let loadingMore = $state(false); |
|
|
|
|
let hasMoreEvents = $state(true); |
|
|
|
|
|
|
|
|
|
// Subscription |
|
|
|
|
let subscriptionId: string | null = $state(null); |
|
|
|
|
let isMounted = $state(true); |
|
|
|
|
let loadingParents = $state(false); // Guard to prevent concurrent parent/quoted event loads |
|
|
|
|
let loadingFeed = $state(false); // Guard to prevent concurrent feed loads |
|
|
|
|
let pendingSubscriptionEvents = $state<NostrEvent[]>([]); // Batch subscription events |
|
|
|
|
let subscriptionBatchTimeout: ReturnType<typeof setTimeout> | null = null; |
|
|
|
|
let initialLoadComplete = $state(false); |
|
|
|
|
|
|
|
|
|
function openDrawer(event: NostrEvent) { |
|
|
|
|
drawerEvent = event; |
|
|
|
|
@ -44,211 +45,118 @@
@@ -44,211 +45,118 @@
|
|
|
|
|
drawerEvent = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
onMount(() => { |
|
|
|
|
isMounted = true; |
|
|
|
|
(async () => { |
|
|
|
|
await nostrClient.initialize(); |
|
|
|
|
if (!isMounted) return; |
|
|
|
|
|
|
|
|
|
// Load cached feed events immediately (15 minute cache) |
|
|
|
|
await loadCachedFeed(); |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; |
|
|
|
|
// Then fetch fresh data in the background |
|
|
|
|
await loadFeed(); |
|
|
|
|
if (!isMounted) return; |
|
|
|
|
setupSubscription(); |
|
|
|
|
})(); |
|
|
|
|
|
|
|
|
|
return () => { |
|
|
|
|
isMounted = false; |
|
|
|
|
if (subscriptionId) { |
|
|
|
|
nostrClient.unsubscribe(subscriptionId); |
|
|
|
|
subscriptionId = null; |
|
|
|
|
} |
|
|
|
|
if (subscriptionBatchTimeout) { |
|
|
|
|
clearTimeout(subscriptionBatchTimeout); |
|
|
|
|
subscriptionBatchTimeout = null; |
|
|
|
|
} |
|
|
|
|
pendingSubscriptionEvents = []; |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Listen for custom event from EmbeddedEvent components |
|
|
|
|
$effect(() => { |
|
|
|
|
const handleOpenEvent = (e: CustomEvent) => { |
|
|
|
|
if (e.detail?.event) { |
|
|
|
|
openDrawer(e.detail.event); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
window.addEventListener('openEventInDrawer', handleOpenEvent as EventListener); |
|
|
|
|
|
|
|
|
|
return () => { |
|
|
|
|
window.removeEventListener('openEventInDrawer', handleOpenEvent as EventListener); |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
function processSubscriptionBatch() { |
|
|
|
|
if (!isMounted || pendingSubscriptionEvents.length === 0 || loadingFeed) { |
|
|
|
|
pendingSubscriptionEvents = []; |
|
|
|
|
subscriptionBatchTimeout = null; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Get current events snapshot to avoid race conditions |
|
|
|
|
const currentEvents = events; |
|
|
|
|
const currentEventIds = new Set(currentEvents.map(e => e.id)); |
|
|
|
|
|
|
|
|
|
// Filter out discussion threads and deduplicate within pending events |
|
|
|
|
const seenInPending = new Set<string>(); |
|
|
|
|
const newEvents: NostrEvent[] = []; |
|
|
|
|
|
|
|
|
|
for (const event of pendingSubscriptionEvents) { |
|
|
|
|
// Skip discussion threads |
|
|
|
|
if (event.kind === KIND.DISCUSSION_THREAD) continue; |
|
|
|
|
|
|
|
|
|
// Skip if already in current events or already seen in this batch |
|
|
|
|
if (!currentEventIds.has(event.id) && !seenInPending.has(event.id)) { |
|
|
|
|
newEvents.push(event); |
|
|
|
|
seenInPending.add(event.id); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (newEvents.length === 0) { |
|
|
|
|
pendingSubscriptionEvents = []; |
|
|
|
|
subscriptionBatchTimeout = null; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Create a completely new events array with all unique events |
|
|
|
|
// Use a Map to ensure uniqueness by event ID (last one wins if duplicates somehow exist) |
|
|
|
|
const eventsMap = new Map<string, NostrEvent>(); |
|
|
|
|
|
|
|
|
|
// Add all current events first |
|
|
|
|
for (const event of currentEvents) { |
|
|
|
|
eventsMap.set(event.id, event); |
|
|
|
|
} |
|
|
|
|
// Load waiting room events into feed |
|
|
|
|
function loadWaitingRoomEvents() { |
|
|
|
|
if (waitingRoomEvents.length === 0) return; |
|
|
|
|
|
|
|
|
|
// Add new events (will overwrite if somehow duplicate, but shouldn't happen) |
|
|
|
|
for (const event of newEvents) { |
|
|
|
|
eventsMap.set(event.id, event); |
|
|
|
|
const eventMap = new Map(events.map(e => [e.id, e])); |
|
|
|
|
for (const event of waitingRoomEvents) { |
|
|
|
|
eventMap.set(event.id, event); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Convert to array and sort |
|
|
|
|
events = Array.from(eventsMap.values()).sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
|
|
|
|
|
pendingSubscriptionEvents = []; |
|
|
|
|
subscriptionBatchTimeout = null; |
|
|
|
|
events = Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
waitingRoomEvents = []; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function setupSubscription() { |
|
|
|
|
if (subscriptionId || singleRelay) return; |
|
|
|
|
// Load older events (pagination) |
|
|
|
|
async function loadOlderEvents() { |
|
|
|
|
if (loadingMore || !hasMoreEvents) return; |
|
|
|
|
|
|
|
|
|
const relays = relayManager.getFeedReadRelays(); |
|
|
|
|
const feedKinds = getFeedKinds().filter(kind => kind !== KIND.DISCUSSION_THREAD); |
|
|
|
|
const filters = feedKinds.map(kind => ({ kinds: [kind], limit: config.feedLimit })); |
|
|
|
|
const untilTimestamp = oldestTimestamp ?? Math.floor(Date.now() / 1000); |
|
|
|
|
if (!untilTimestamp) return; |
|
|
|
|
|
|
|
|
|
subscriptionId = nostrClient.subscribe( |
|
|
|
|
loadingMore = true; |
|
|
|
|
try { |
|
|
|
|
const relays = singleRelay ? [singleRelay] : relayManager.getFeedReadRelays(); |
|
|
|
|
const feedKinds = getFeedKinds().filter(k => k !== KIND.DISCUSSION_THREAD); |
|
|
|
|
const filters = feedKinds.map(k => ({ |
|
|
|
|
kinds: [k], |
|
|
|
|
limit: config.feedLimit, |
|
|
|
|
until: untilTimestamp - 1 |
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
const fetched = await nostrClient.fetchEvents( |
|
|
|
|
filters, |
|
|
|
|
relays, |
|
|
|
|
(event: NostrEvent) => { |
|
|
|
|
if (!isMounted || event.kind === KIND.DISCUSSION_THREAD || loadingFeed) return; |
|
|
|
|
|
|
|
|
|
// Add to pending batch |
|
|
|
|
pendingSubscriptionEvents.push(event); |
|
|
|
|
|
|
|
|
|
// Clear existing timeout |
|
|
|
|
if (subscriptionBatchTimeout) { |
|
|
|
|
clearTimeout(subscriptionBatchTimeout); |
|
|
|
|
{ |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: config.standardTimeout |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Process batch after a short delay (debounce rapid updates) |
|
|
|
|
subscriptionBatchTimeout = setTimeout(() => { |
|
|
|
|
processSubscriptionBatch(); |
|
|
|
|
}, 100); // 100ms debounce |
|
|
|
|
}, |
|
|
|
|
() => {} |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; |
|
|
|
|
|
|
|
|
|
async function loadCachedFeed() { |
|
|
|
|
if (!isMounted || singleRelay) return; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const feedKinds = getFeedKinds().filter(kind => kind !== KIND.DISCUSSION_THREAD); |
|
|
|
|
// Load events cached within the last 15 minutes |
|
|
|
|
const cachedEvents = await getRecentFeedEvents(feedKinds, 15 * 60 * 1000, 50); |
|
|
|
|
|
|
|
|
|
if (cachedEvents.length > 0 && isMounted) { |
|
|
|
|
// Filter to only showInFeed kinds and exclude kind 11 |
|
|
|
|
const filteredEvents = cachedEvents.filter((e: NostrEvent) => |
|
|
|
|
const filtered = fetched.filter(e => |
|
|
|
|
e.kind !== KIND.DISCUSSION_THREAD && |
|
|
|
|
getKindInfo(e.kind).showInFeed === true |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Deduplicate |
|
|
|
|
const uniqueMap = new Map<string, NostrEvent>(); |
|
|
|
|
for (const event of filteredEvents) { |
|
|
|
|
if (!uniqueMap.has(event.id)) { |
|
|
|
|
uniqueMap.set(event.id, event); |
|
|
|
|
} |
|
|
|
|
if (filtered.length === 0) { |
|
|
|
|
hasMoreEvents = false; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const sortedEvents = Array.from(uniqueMap.values()).sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
|
|
|
|
|
if (sortedEvents.length > 0) { |
|
|
|
|
events = sortedEvents; |
|
|
|
|
// Load parent/quoted events in background, don't await |
|
|
|
|
// Only load if not already loading to prevent cascading fetches |
|
|
|
|
if (!loadingParents) { |
|
|
|
|
loadParentAndQuotedEvents(events).catch(err => { |
|
|
|
|
console.error('Error loading parent/quoted events from cache:', err); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
// Don't set loading to false here - let loadFeed() handle that |
|
|
|
|
} |
|
|
|
|
const eventMap = new Map(events.map(e => [e.id, e])); |
|
|
|
|
for (const event of filtered) { |
|
|
|
|
eventMap.set(event.id, event); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const sorted = Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
events = sorted; |
|
|
|
|
oldestTimestamp = Math.min(...sorted.map(e => e.created_at)); |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading cached feed:', error); |
|
|
|
|
// Don't set loading to false - let loadFeed() handle that |
|
|
|
|
console.error('Error loading older events:', error); |
|
|
|
|
} finally { |
|
|
|
|
loadingMore = false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Initial feed load |
|
|
|
|
async function loadFeed() { |
|
|
|
|
if (!isMounted || loadingFeed) return; // Prevent concurrent loads |
|
|
|
|
if (!isMounted) return; |
|
|
|
|
|
|
|
|
|
loadingFeed = true; |
|
|
|
|
// Only show loading spinner if we don't have cached events |
|
|
|
|
const hasCachedEvents = events.length > 0; |
|
|
|
|
if (!hasCachedEvents) { |
|
|
|
|
loading = true; |
|
|
|
|
} |
|
|
|
|
relayError = null; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// Load from cache first (fast) |
|
|
|
|
if (!singleRelay) { |
|
|
|
|
const feedKinds = getFeedKinds().filter(k => k !== KIND.DISCUSSION_THREAD); |
|
|
|
|
const cached = await getRecentFeedEvents(feedKinds, 15 * 60 * 1000, config.feedLimit); |
|
|
|
|
const filtered = cached.filter(e => |
|
|
|
|
e.kind !== KIND.DISCUSSION_THREAD && |
|
|
|
|
getKindInfo(e.kind).showInFeed === true |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (filtered.length > 0 && isMounted) { |
|
|
|
|
const unique = Array.from(new Map(filtered.map(e => [e.id, e])).values()); |
|
|
|
|
events = unique.sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
oldestTimestamp = Math.min(...events.map(e => e.created_at)); |
|
|
|
|
loading = false; // Show cached content immediately |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fetch fresh data from relays (can be slow) |
|
|
|
|
const relays = singleRelay ? [singleRelay] : relayManager.getFeedReadRelays(); |
|
|
|
|
|
|
|
|
|
if (singleRelay) { |
|
|
|
|
try { |
|
|
|
|
const relay = await nostrClient.getRelay(singleRelay); |
|
|
|
|
if (!relay) { |
|
|
|
|
relayError = `Relay ${singleRelay} is unavailable or returned an error.`; |
|
|
|
|
loading = false; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
relayError = `Failed to connect to relay ${singleRelay}: ${error instanceof Error ? error.message : 'Unknown error'}`; |
|
|
|
|
relayError = `Relay ${singleRelay} is unavailable.`; |
|
|
|
|
loading = false; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const feedKinds = getFeedKinds().filter(kind => kind !== KIND.DISCUSSION_THREAD); |
|
|
|
|
const filters = feedKinds.map(kind => ({ kinds: [kind], limit: config.feedLimit })); |
|
|
|
|
const feedKinds = getFeedKinds().filter(k => k !== KIND.DISCUSSION_THREAD); |
|
|
|
|
const filters = feedKinds.map(k => ({ kinds: [k], limit: config.feedLimit })); |
|
|
|
|
|
|
|
|
|
const fetchOptions = singleRelay ? { |
|
|
|
|
const fetched = await nostrClient.fetchEvents( |
|
|
|
|
filters, |
|
|
|
|
relays, |
|
|
|
|
singleRelay ? { |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: false, |
|
|
|
|
cacheResults: false, |
|
|
|
|
@ -258,140 +166,97 @@
@@ -258,140 +166,97 @@
|
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: config.standardTimeout |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const fetchedEvents = await nostrClient.fetchEvents(filters, relays, fetchOptions); |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; |
|
|
|
|
|
|
|
|
|
// Filter to only showInFeed kinds and exclude kind 11 |
|
|
|
|
const filteredEvents = fetchedEvents.filter((e: NostrEvent) => |
|
|
|
|
const filtered = fetched.filter(e => |
|
|
|
|
e.kind !== KIND.DISCUSSION_THREAD && |
|
|
|
|
getKindInfo(e.kind).showInFeed === true |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Deduplicate and merge with existing events |
|
|
|
|
const existingIds = new Set(events.map(e => e.id)); |
|
|
|
|
const uniqueMap = new Map<string, NostrEvent>(); |
|
|
|
|
|
|
|
|
|
// Add existing events first |
|
|
|
|
for (const event of events) { |
|
|
|
|
uniqueMap.set(event.id, event); |
|
|
|
|
const eventMap = new Map(events.map(e => [e.id, e])); |
|
|
|
|
for (const event of filtered) { |
|
|
|
|
eventMap.set(event.id, event); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Add new events |
|
|
|
|
for (const event of filteredEvents) { |
|
|
|
|
if (!uniqueMap.has(event.id)) { |
|
|
|
|
uniqueMap.set(event.id, event); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
events = Array.from(uniqueMap.values()).sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
const sorted = Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
events = sorted; |
|
|
|
|
|
|
|
|
|
if (events.length > 0) { |
|
|
|
|
// Load parent/quoted events in background, don't await |
|
|
|
|
// Only load if not already loading to prevent cascading fetches |
|
|
|
|
if (!loadingParents) { |
|
|
|
|
loadParentAndQuotedEvents(events).catch(err => { |
|
|
|
|
console.error('Error loading parent/quoted events:', err); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
if (sorted.length > 0) { |
|
|
|
|
oldestTimestamp = Math.min(...sorted.map(e => e.created_at)); |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading feed:', error); |
|
|
|
|
if (!events.length) { |
|
|
|
|
relayError = 'Failed to load feed.'; |
|
|
|
|
} |
|
|
|
|
} finally { |
|
|
|
|
loading = false; |
|
|
|
|
loadingFeed = false; |
|
|
|
|
initialLoadComplete = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Setup subscription (only adds to waiting room) |
|
|
|
|
function setupSubscription() { |
|
|
|
|
if (subscriptionId || singleRelay) return; |
|
|
|
|
|
|
|
|
|
async function loadParentAndQuotedEvents(postsToLoad: NostrEvent[]) { |
|
|
|
|
if (!isMounted || postsToLoad.length === 0 || loadingParents) return; |
|
|
|
|
|
|
|
|
|
loadingParents = true; |
|
|
|
|
try { |
|
|
|
|
const relays = singleRelay ? [singleRelay] : relayManager.getFeedReadRelays(); |
|
|
|
|
const parentEventIds = new Set<string>(); |
|
|
|
|
const quotedEventIds = new Set<string>(); |
|
|
|
|
|
|
|
|
|
for (const post of postsToLoad) { |
|
|
|
|
const replyTag = post.tags.find((t) => t[0] === 'e' && t[3] === 'reply'); |
|
|
|
|
if (replyTag && replyTag[1]) { |
|
|
|
|
parentEventIds.add(replyTag[1]); |
|
|
|
|
} else { |
|
|
|
|
const rootId = post.tags.find((t) => t[0] === 'root')?.[1]; |
|
|
|
|
const eTag = post.tags.find((t) => t[0] === 'e' && t[1] !== rootId && t[1] !== post.id); |
|
|
|
|
if (eTag && eTag[1]) { |
|
|
|
|
parentEventIds.add(eTag[1]); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const quotedTag = post.tags.find((t) => t[0] === 'q'); |
|
|
|
|
if (quotedTag && quotedTag[1]) { |
|
|
|
|
quotedEventIds.add(quotedTag[1]); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const allEventIds = [...parentEventIds, ...quotedEventIds]; |
|
|
|
|
if (allEventIds.length === 0) return; |
|
|
|
|
const relays = relayManager.getFeedReadRelays(); |
|
|
|
|
const feedKinds = getFeedKinds().filter(k => k !== KIND.DISCUSSION_THREAD); |
|
|
|
|
const filters = feedKinds.map(k => ({ kinds: [k], limit: config.feedLimit })); |
|
|
|
|
|
|
|
|
|
const fetchedEvents = await nostrClient.fetchEvents( |
|
|
|
|
[{ kinds: [KIND.SHORT_TEXT_NOTE], ids: allEventIds }], |
|
|
|
|
subscriptionId = nostrClient.subscribe( |
|
|
|
|
filters, |
|
|
|
|
relays, |
|
|
|
|
singleRelay ? { |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: false, |
|
|
|
|
cacheResults: false, |
|
|
|
|
timeout: config.standardTimeout |
|
|
|
|
} : { |
|
|
|
|
relayFirst: true, |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
timeout: config.standardTimeout |
|
|
|
|
(event: NostrEvent) => { |
|
|
|
|
if (!isMounted || event.kind === KIND.DISCUSSION_THREAD || !initialLoadComplete) return; |
|
|
|
|
|
|
|
|
|
// Add to waiting room if not already in feed or waiting room |
|
|
|
|
const eventIds = new Set([...events.map(e => e.id), ...waitingRoomEvents.map(e => e.id)]); |
|
|
|
|
if (!eventIds.has(event.id)) { |
|
|
|
|
waitingRoomEvents = [...waitingRoomEvents, event].sort((a, b) => b.created_at - a.created_at); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
() => {} |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (!isMounted) return; |
|
|
|
|
|
|
|
|
|
const eventsById = new Map<string, NostrEvent>(); |
|
|
|
|
for (const event of fetchedEvents) { |
|
|
|
|
eventsById.set(event.id, event); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for (const post of postsToLoad) { |
|
|
|
|
const replyTag = post.tags.find((t) => t[0] === 'e' && t[3] === 'reply'); |
|
|
|
|
let parentId: string | undefined; |
|
|
|
|
if (replyTag && replyTag[1]) { |
|
|
|
|
parentId = replyTag[1]; |
|
|
|
|
} else { |
|
|
|
|
const rootId = post.tags.find((t) => t[0] === 'root')?.[1]; |
|
|
|
|
const eTag = post.tags.find((t) => t[0] === 'e' && t[1] !== rootId && t[1] !== post.id); |
|
|
|
|
parentId = eTag?.[1]; |
|
|
|
|
onMount(() => { |
|
|
|
|
isMounted = true; |
|
|
|
|
nostrClient.initialize().then(() => { |
|
|
|
|
if (isMounted) { |
|
|
|
|
loadFeed().then(() => { |
|
|
|
|
if (isMounted) { |
|
|
|
|
setupSubscription(); |
|
|
|
|
} |
|
|
|
|
if (parentId && eventsById.has(parentId)) { |
|
|
|
|
parentEventsMap.set(post.id, eventsById.get(parentId)!); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const quotedTag = post.tags.find((t) => t[0] === 'q'); |
|
|
|
|
if (quotedTag && quotedTag[1] && eventsById.has(quotedTag[1])) { |
|
|
|
|
quotedEventsMap.set(post.id, eventsById.get(quotedTag[1])!); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('[FeedPage] Error loading parent/quoted events:', error); |
|
|
|
|
} finally { |
|
|
|
|
loadingParents = false; |
|
|
|
|
} |
|
|
|
|
return () => { |
|
|
|
|
isMounted = false; |
|
|
|
|
if (subscriptionId) { |
|
|
|
|
nostrClient.unsubscribe(subscriptionId); |
|
|
|
|
subscriptionId = null; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Listen for drawer events |
|
|
|
|
$effect(() => { |
|
|
|
|
const handler = (e: CustomEvent) => { |
|
|
|
|
if (e.detail?.event) openDrawer(e.detail.event); |
|
|
|
|
}; |
|
|
|
|
window.addEventListener('openEventInDrawer', handler as EventListener); |
|
|
|
|
return () => window.removeEventListener('openEventInDrawer', handler as EventListener); |
|
|
|
|
}); |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<div class="feed-page"> |
|
|
|
|
{#if singleRelay} |
|
|
|
|
<div class="relay-info"> |
|
|
|
|
<p class="relay-info-text"> |
|
|
|
|
Showing feed from: <code class="relay-url">{singleRelay}</code> |
|
|
|
|
</p> |
|
|
|
|
<p class="relay-info-text">Showing feed from: <code class="relay-url">{singleRelay}</code></p> |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
@ -408,17 +273,30 @@
@@ -408,17 +273,30 @@
|
|
|
|
|
<p class="text-fog-text dark:text-fog-dark-text">No posts found.</p> |
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
{#if waitingRoomEvents.length > 0} |
|
|
|
|
<div class="waiting-room-banner"> |
|
|
|
|
<button onclick={loadWaitingRoomEvents} class="see-new-events-btn"> |
|
|
|
|
See {waitingRoomEvents.length} new event{waitingRoomEvents.length === 1 ? '' : 's'} |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
<div class="feed-posts"> |
|
|
|
|
{#each events as event (event.id)} |
|
|
|
|
<FeedPost |
|
|
|
|
post={event} |
|
|
|
|
onOpenEvent={openDrawer} |
|
|
|
|
parentEvent={parentEventsMap.get(event.id)} |
|
|
|
|
quotedEvent={quotedEventsMap.get(event.id)} |
|
|
|
|
/> |
|
|
|
|
<FeedPost post={event} onOpenEvent={openDrawer} /> |
|
|
|
|
{/each} |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div class="load-more-section"> |
|
|
|
|
<button |
|
|
|
|
onclick={loadOlderEvents} |
|
|
|
|
disabled={loadingMore || !hasMoreEvents} |
|
|
|
|
class="see-more-events-btn" |
|
|
|
|
> |
|
|
|
|
{loadingMore ? 'Loading...' : 'See more events'} |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{#if drawerOpen && drawerEvent} |
|
|
|
|
<ThreadDrawer opEvent={drawerEvent} isOpen={drawerOpen} onClose={closeDrawer} /> |
|
|
|
|
{/if} |
|
|
|
|
@ -489,21 +367,57 @@
@@ -489,21 +367,57 @@
|
|
|
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Responsive images and media in feed */ |
|
|
|
|
:global(.feed-page img):not(.profile-picture) { |
|
|
|
|
max-width: 600px; |
|
|
|
|
width: 100%; |
|
|
|
|
height: auto; |
|
|
|
|
.waiting-room-banner { |
|
|
|
|
padding: 1rem; |
|
|
|
|
margin-bottom: 1rem; |
|
|
|
|
background: var(--fog-highlight, #f3f4f6); |
|
|
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
border-radius: 0.375rem; |
|
|
|
|
text-align: center; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .waiting-room-banner { |
|
|
|
|
background: var(--fog-dark-highlight, #374151); |
|
|
|
|
border-color: var(--fog-dark-border, #475569); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.feed-page video) { |
|
|
|
|
max-width: 600px; |
|
|
|
|
width: 100%; |
|
|
|
|
height: auto; |
|
|
|
|
.see-new-events-btn, |
|
|
|
|
.see-more-events-btn { |
|
|
|
|
padding: 0.75rem 1.5rem; |
|
|
|
|
background: var(--fog-accent, #64748b); |
|
|
|
|
color: white; |
|
|
|
|
border: none; |
|
|
|
|
border-radius: 0.375rem; |
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
font-weight: 500; |
|
|
|
|
cursor: pointer; |
|
|
|
|
transition: all 0.2s; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.feed-page audio) { |
|
|
|
|
max-width: 600px; |
|
|
|
|
width: 100%; |
|
|
|
|
.see-new-events-btn:hover, |
|
|
|
|
.see-more-events-btn:hover:not(:disabled) { |
|
|
|
|
background: var(--fog-text, #475569); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.see-new-events-btn:disabled, |
|
|
|
|
.see-more-events-btn:disabled { |
|
|
|
|
opacity: 0.5; |
|
|
|
|
cursor: not-allowed; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .see-new-events-btn, |
|
|
|
|
:global(.dark) .see-more-events-btn { |
|
|
|
|
background: var(--fog-dark-accent, #94a3b8); |
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .see-new-events-btn:hover:not(:disabled), |
|
|
|
|
:global(.dark) .see-more-events-btn:hover:not(:disabled) { |
|
|
|
|
background: var(--fog-dark-text, #cbd5e1); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.load-more-section { |
|
|
|
|
padding: 2rem; |
|
|
|
|
text-align: center; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
|