diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 1ecc62d0..b73e782f 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -578,7 +578,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const savedFeedStateRef = useRef>(new Map()) const currentTabStateRef = useRef>(new Map()) // Track current tab state for each page @@ -610,7 +610,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } // Get trending tab if on search page - const trendingTab = currentTabStateRef.current.get('search') as 'relays' | 'hashtags' | undefined + const trendingTab = currentTabStateRef.current.get('search') as 'relays' | 'hashtags' | 'calendar' | undefined // Save state (tab, discussions, trending) if any exists if (currentTab || discussionsState || trendingTab) { @@ -1181,7 +1181,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // Save tab state before navigating const currentTab = currentTabStateRef.current.get(currentPrimaryPage) - const trendingTab = currentTabStateRef.current.get('search') as 'relays' | 'hashtags' | undefined + const trendingTab = currentTabStateRef.current.get('search') as 'relays' | 'hashtags' | 'calendar' | undefined if (currentPrimaryPage && (currentTab || trendingTab)) { logger.info('PageManager: Desktop - Saving page state', { diff --git a/src/components/Embedded/EmbeddedCalendarEvent.tsx b/src/components/Embedded/EmbeddedCalendarEvent.tsx index 30870370..2b146618 100644 --- a/src/components/Embedded/EmbeddedCalendarEvent.tsx +++ b/src/components/Embedded/EmbeddedCalendarEvent.tsx @@ -1,4 +1,3 @@ -import { ExtendedKind } from '@/constants' import { getCalendarEventMeta, formatCalendarTime, diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index 12138e8d..df1afc0f 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -20,6 +20,7 @@ import React, { useMemo, useState, useCallback, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' import Lightbox from 'yet-another-react-lightbox' import Zoom from 'yet-another-react-lightbox/plugins/zoom' +import CalendarEventContent from '@/components/CalendarEventContent' import { EmbeddedNote, EmbeddedMention } from '@/components/Embedded' import EmbeddedCitation from '@/components/EmbeddedCitation' import { preprocessMarkdownMediaLinks } from './preprocessMarkup' @@ -422,9 +423,11 @@ function parseMarkdownContent( imageThumbnailMap?: Map getImageIdentifier?: (url: string) => string | null emojiInfos?: TEmoji[] + /** When viewing a kind-24 invite, render full calendar card with RSVP instead of EmbeddedNote for this naddr */ + fullCalendarInvite?: { naddr: string; event: Event } } ): { nodes: React.ReactNode[]; hashtagsInContent: Set; footnotes: Map; citations: Array<{ id: string; type: string; citationId: string }> } { - const { eventPubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay, videoPosterMap, imageThumbnailMap, getImageIdentifier, emojiInfos = [] } = options + const { eventPubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay, videoPosterMap, imageThumbnailMap, getImageIdentifier, emojiInfos = [], fullCalendarInvite } = options const parts: React.ReactNode[] = [] const hashtagsInContent = new Set() const footnotes = new Map() @@ -2154,12 +2157,21 @@ function parseMarkdownContent( ) } else if (bech32Id.startsWith('note') || bech32Id.startsWith('nevent') || bech32Id.startsWith('naddr')) { - // Embedded events should be block-level and fill width - parts.push( -
- -
- ) + // When this is the calendar invite naddr, show full calendar card with RSVP instead of embedded preview + if (fullCalendarInvite && fullCalendarInvite.naddr === bech32Id) { + parts.push( +
+ +
+ ) + } else { + // Embedded events should be block-level and fill width + parts.push( +
+ +
+ ) + } } else { parts.push(nostr:{bech32Id}) } @@ -3158,12 +3170,15 @@ export default function MarkdownArticle({ event, className, hideMetadata = false, - parentImageUrl + parentImageUrl, + fullCalendarInvite }: { event: Event className?: string hideMetadata?: boolean parentImageUrl?: string + /** When viewing a kind-24 invite, render full calendar card with RSVP in place of the naddr embed */ + fullCalendarInvite?: { naddr: string; event: Event } }) { const { push } = useSecondaryPage() const { navigateToHashtag } = useSmartHashtagNavigation() @@ -3513,11 +3528,12 @@ export default function MarkdownArticle({ videoPosterMap, imageThumbnailMap, getImageIdentifier, - emojiInfos + emojiInfos, + fullCalendarInvite }) // Return nodes and hashtags (footnotes are already included in nodes) return { nodes: result.nodes, hashtagsInContent: result.hashtagsInContent } - }, [preprocessedContent, event.pubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay, videoPosterMap, imageThumbnailMap, getImageIdentifier, emojiInfos]) + }, [preprocessedContent, event.pubkey, imageIndexMap, openLightbox, navigateToHashtag, navigateToRelay, videoPosterMap, imageThumbnailMap, getImageIdentifier, emojiInfos, fullCalendarInvite]) // Filter metadata tags to only show what's not already in content const leftoverMetadataTags = useMemo(() => { diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index fdf35552..77fc4968 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -51,7 +51,8 @@ export default function Note({ className, hideParentNotePreview = false, showFull = false, - disableClick = false + disableClick = false, + fullCalendarInvite }: { event: Event originalNoteId?: string @@ -60,6 +61,8 @@ export default function Note({ hideParentNotePreview?: boolean showFull?: boolean disableClick?: boolean + /** When viewing a kind-24 invite, use this to replace the embedded calendar with the full card (RSVP) in content */ + fullCalendarInvite?: { event: Event; naddr: string } }) { const { navigateToNote } = useSmartNoteNavigation() const { isSmallScreen } = useScreenSize() @@ -220,7 +223,14 @@ export default function Note({ } else if (event.kind === ExtendedKind.CALENDAR_EVENT_TIME || event.kind === ExtendedKind.CALENDAR_EVENT_DATE) { content = } else if (event.kind === ExtendedKind.PUBLIC_MESSAGE) { - content = + content = ( + + ) } else if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) { content = } else if (event.kind === ExtendedKind.FOLLOW_PACK) { diff --git a/src/components/TrendingNotes/index.tsx b/src/components/TrendingNotes/index.tsx index e5fa1fe2..44967133 100644 --- a/src/components/TrendingNotes/index.tsx +++ b/src/components/TrendingNotes/index.tsx @@ -1,4 +1,5 @@ import NoteCard, { NoteCardLoadingSkeleton } from '@/components/NoteCard' +import { ExtendedKind } from '@/constants' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useUserTrust } from '@/providers/UserTrustProvider' @@ -13,6 +14,7 @@ import noteStatsService from '@/services/note-stats.service' import { FAST_READ_RELAY_URLS } from '@/constants' import logger from '@/lib/logger' import { normalizeUrl } from '@/lib/url' +import { getCalendarEventMeta } from '@/lib/calendar-event' const SHOW_COUNT = 25 const CACHE_DURATION = 30 * 60 * 1000 // 30 minutes @@ -27,10 +29,45 @@ let cachedCustomEvents: { // Flag to prevent concurrent initialization let isInitializing = false -type TrendingTab = 'relays' | 'hashtags' +type TrendingTab = 'relays' | 'hashtags' | 'calendar' type SortOrder = 'newest' | 'oldest' | 'most-popular' | 'least-popular' type HashtagFilter = 'popular' +/** Sort key for calendar events: time-based use start (unix), date-based use startDate as timestamp. */ +function calendarEventSortKey(evt: NostrEvent): number { + const meta = getCalendarEventMeta(evt as any) + if (meta.start != null && !isNaN(meta.start)) return meta.start + if (meta.startDate) return new Date(meta.startDate + 'T00:00:00').getTime() / 1000 + return evt.created_at +} + +const CALENDAR_MONTHS_AHEAD = 6 + +/** YYYY-MM for grouping; derived from calendar event start. */ +function calendarEventMonthKey(evt: NostrEvent): string { + const ts = calendarEventSortKey(evt) + const d = new Date(ts * 1000) + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + return `${y}-${m}` +} + +/** Filter calendar events: from start of today (or 1 month ago if in past) through the next CALENDAR_MONTHS_AHEAD months. */ +function filterCalendarEventsToNextMonths(events: NostrEvent[], monthsAhead: number): NostrEvent[] { + const startOfToday = new Date() + startOfToday.setHours(0, 0, 0, 0) + const oneMonthAgo = new Date(startOfToday) + oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1) + const minSec = Math.floor(oneMonthAgo.getTime() / 1000) + const end = new Date() + end.setMonth(end.getMonth() + monthsAhead) + const endSec = Math.floor(end.getTime() / 1000) + return events.filter((evt) => { + const k = calendarEventSortKey(evt) + return k >= minSec && k <= endSec + }) +} + export default function TrendingNotes() { const { t } = useTranslation() const { isEventDeleted } = useDeletedEvent() @@ -46,12 +83,14 @@ export default function TrendingNotes() { const [popularHashtags, setPopularHashtags] = useState([]) const [cacheEvents, setCacheEvents] = useState([]) const [cacheLoading, setCacheLoading] = useState(false) + const [calendarEvents, setCalendarEvents] = useState([]) + const [calendarLoading, setCalendarLoading] = useState(false) const bottomRef = useRef(null) // Listen for tab restoration from PageManager useEffect(() => { const handleRestore = (e: CustomEvent<{ page: string, tab: string }>) => { - if (e.detail.page === 'search' && e.detail.tab && ['relays', 'hashtags'].includes(e.detail.tab)) { + if (e.detail.page === 'search' && e.detail.tab && ['relays', 'hashtags', 'calendar'].includes(e.detail.tab)) { setActiveTab(e.detail.tab as TrendingTab) } } @@ -366,6 +405,66 @@ export default function TrendingNotes() { }, []) // Only run once on mount to prevent infinite loop + // Fetch calendar events when calendar tab is active. Use same filters as profile/notifications: by author and by invitee (#p). + useEffect(() => { + if (activeTab !== 'calendar') return + const userRelays = getRelays ?? [] + const relaySet = new Set([ + ...userRelays.map((url) => normalizeUrl(url) || url).filter(Boolean), + ...FAST_READ_RELAY_URLS.map((url) => normalizeUrl(url) || url).filter(Boolean) + ]) + if (relayList?.write?.length) { + relayList.write.forEach((url) => { + const u = normalizeUrl(url) + if (u) relaySet.add(u) + }) + } + const relays = Array.from(relaySet) + if (relays.length === 0) { + setCalendarLoading(false) + return + } + let cancelled = false + setCalendarLoading(true) + const run = async () => { + try { + const calendarKinds = [ExtendedKind.CALENDAR_EVENT_DATE, ExtendedKind.CALENDAR_EVENT_TIME] + // Same query pattern as profile timeline: events you created + events you're invited to. Relays respond to these; global kind-only often returns nothing. + const filters = pubkey + ? [ + { kinds: calendarKinds, authors: [pubkey], limit: 100 }, + { kinds: calendarKinds, '#p': [pubkey], limit: 100 } + ] + : [{ kinds: calendarKinds, limit: 200 }] + const events = await client.fetchEvents(relays, filters, { + eoseTimeout: 8000, + globalTimeout: 20000 + }) + if (cancelled) return + const seen = new Set() + const deduped: NostrEvent[] = [] + events.forEach((evt) => { + const id = isReplaceableEvent((evt as any).kind) ? getReplaceableCoordinateFromEvent(evt as any) : (evt as any).id + if (!seen.has(id)) { + seen.add(id) + deduped.push(evt) + } + }) + const inRange = filterCalendarEventsToNextMonths(deduped, CALENDAR_MONTHS_AHEAD) + inRange.sort((a, b) => calendarEventSortKey(a) - calendarEventSortKey(b)) + setCalendarEvents(inRange) + } catch (e) { + if (!cancelled) setCalendarEvents([]) + } finally { + if (!cancelled) setCalendarLoading(false) + } + } + run() + return () => { + cancelled = true + } + }, [activeTab, getRelays, relayList?.write, pubkey]) + // Compute filtered events without slicing (for pagination length check) const relaysFilteredEventsAll = useMemo(() => { const idSet = new Set() @@ -467,9 +566,25 @@ export default function TrendingNotes() { return relaysFilteredEventsAll.slice(0, showCount) }, [relaysFilteredEventsAll, showCount]) + // For calendar tab: group events by month (YYYY-MM), months in order; for others use relays + const calendarEventsByMonth = useMemo(() => { + const byMonth = new Map() + calendarEvents.forEach((evt) => { + const key = calendarEventMonthKey(evt) + if (!byMonth.has(key)) byMonth.set(key, []) + byMonth.get(key)!.push(evt) + }) + byMonth.forEach((list) => list.sort((a, b) => calendarEventSortKey(a) - calendarEventSortKey(b))) + const monthKeys = Array.from(byMonth.keys()).sort() + return { monthKeys, byMonth } + }, [calendarEvents]) + const filteredEvents = useMemo(() => { + if (activeTab === 'calendar') { + return calendarEvents.slice(0, showCount) + } return relaysFilteredEvents - }, [relaysFilteredEvents]) + }, [activeTab, calendarEvents, relaysFilteredEvents, showCount]) @@ -562,10 +677,25 @@ export default function TrendingNotes() { > hashtags + - {/* Second row controls for tabs 2-3 */} + {/* Second row controls for relays / hashtags (calendar has no sort – ordered by datetime) */} {(activeTab === 'relays' || activeTab === 'hashtags') && (
{/* Sorting controls - not shown for hashtags tab */} @@ -652,17 +782,59 @@ export default function TrendingNotes() { Loading trending notes from your relays...
)} + {/* Show loading message for calendar tab */} + {activeTab === 'calendar' && calendarLoading && calendarEvents.length === 0 && ( +
+ {t('Loading calendar events...')} +
+ )} + {activeTab === 'calendar' && !calendarLoading && calendarEvents.length === 0 && ( +
+ {t('No calendar events found')} +
+ )} - {filteredEvents.map((event) => ( - - ))} - + {activeTab === 'calendar' + ? calendarEventsByMonth.monthKeys.map((monthKey) => { + const eventsInMonth = calendarEventsByMonth.byMonth.get(monthKey) ?? [] + const [y, m] = monthKey.split('-') + const monthLabel = new Date(parseInt(y, 10), parseInt(m, 10) - 1, 1).toLocaleDateString(undefined, { + month: 'long', + year: 'numeric' + }) + return ( +
+

+ {monthLabel} +

+
+ {eventsInMonth.map((event) => ( + + ))} +
+
+ ) + }) + : filteredEvents.map((event) => ( + + ))} + {(() => { - const actualAvailableLength = relaysFilteredEventsAll.length + const actualAvailableLength = activeTab === 'calendar' ? calendarEvents.length : relaysFilteredEventsAll.length + const isLoading = activeTab === 'relays' ? cacheLoading : activeTab === 'calendar' ? calendarLoading : false + const calendarShowingAll = activeTab === 'calendar' && !calendarLoading const shouldShowLoading = - cacheLoading || - showCount < actualAvailableLength + isLoading || + (activeTab !== 'calendar' && showCount < actualAvailableLength) if (shouldShowLoading) { return ( @@ -671,6 +843,13 @@ export default function TrendingNotes() { ) } + if (calendarShowingAll && calendarEvents.length > 0) { + return ( +
+ {t('Calendar events in the next {{count}} months', { count: CALENDAR_MONTHS_AHEAD })} +
+ ) + } return
{t('no more notes')}
})()} diff --git a/src/hooks/useFetchCalendarRsvps.tsx b/src/hooks/useFetchCalendarRsvps.tsx index 575bc9c6..dec1f489 100644 --- a/src/hooks/useFetchCalendarRsvps.tsx +++ b/src/hooks/useFetchCalendarRsvps.tsx @@ -39,22 +39,40 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { const coordinate = getReplaceableCoordinateFromEvent(calendarEvent) const userRead = relayList?.read ?? [] - const relayUrls = Array.from( - new Set([ - ...FAST_READ_RELAY_URLS.map((url) => normalizeUrl(url) || url), - ...userRead.map((url) => normalizeUrl(url) || url) - ]) - ).filter(Boolean) as string[] + const baseUrls = new Set([ + ...FAST_READ_RELAY_URLS.map((url) => normalizeUrl(url) || url), + ...userRead.map((url) => normalizeUrl(url) || url) + ].filter(Boolean) as string[]) + // Include organizer's relays so RSVPs are found when viewing an attendee's profile (RSVPs are often on organizer's outbox/inbox) + const organizerPubkey = calendarEvent.pubkey client - .fetchEvents(relayUrls, { - kinds: [ExtendedKind.CALENDAR_EVENT_RSVP], - '#a': [coordinate], - limit: 200 + .fetchRelayList(organizerPubkey) + .then((organizerRelays) => { + if (cancelled) return + organizerRelays?.read?.forEach((url) => { + const u = normalizeUrl(url) + if (u) baseUrls.add(u) + }) + organizerRelays?.write?.forEach((url) => { + const u = normalizeUrl(url) + if (u) baseUrls.add(u) + }) + return Array.from(baseUrls) + }) + .catch(() => Array.from(baseUrls)) + .then((relayUrls: string[] | undefined) => { + if (cancelled) return + const urls = relayUrls?.length ? relayUrls : Array.from(baseUrls) + return client.fetchEvents(urls, { + kinds: [ExtendedKind.CALENDAR_EVENT_RSVP], + '#a': [coordinate], + limit: 200 + }) }) .then((events) => { if (cancelled) return - setRsvps(events) + setRsvps(events ?? []) }) .finally(() => { if (!cancelled) setIsFetching(false) @@ -63,7 +81,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { return () => { cancelled = true } - }, [calendarEvent?.id, calendarEvent?.kind, relayList?.read]) + }, [calendarEvent?.id, calendarEvent?.kind, calendarEvent?.pubkey, relayList?.read]) // When we publish an RSVP, NostrProvider calls client.emitNewEvent(event). Merge it into rsvps so the UI updates immediately. useEffect(() => { diff --git a/src/hooks/useProfileTimeline.tsx b/src/hooks/useProfileTimeline.tsx index 8a8f5f0b..d30100ab 100644 --- a/src/hooks/useProfileTimeline.tsx +++ b/src/hooks/useProfileTimeline.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { Event } from 'nostr-tools' import client from '@/services/client.service' -import { FAST_READ_RELAY_URLS } from '@/constants' +import { CALENDAR_EVENT_KINDS, ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' import { normalizeUrl } from '@/lib/url' type ProfileTimelineCacheEntry = { @@ -137,7 +137,8 @@ export function useProfileTimeline({ return } - const subRequests = relayGroups + const hasCalendarKinds = kinds.some((k) => CALENDAR_EVENT_KINDS.includes(k)) + const authorRequests = relayGroups .map((urls) => ({ urls, filter: { @@ -147,6 +148,20 @@ export function useProfileTimeline({ } as any })) .filter((request) => request.urls.length) + // When profile includes calendar event kinds, also subscribe to events where this user is an invitee (#p tag) + const calendarInviteRequests = hasCalendarKinds + ? relayGroups + .map((urls) => ({ + urls, + filter: { + kinds: [ExtendedKind.CALENDAR_EVENT_DATE, ExtendedKind.CALENDAR_EVENT_TIME], + '#p': [pubkey], + limit: 100 + } as any + })) + .filter((request) => request.urls.length) + : [] + const subRequests = [...authorRequests, ...calendarInviteRequests] if (!subRequests.length) { timelineCache.set(cacheKey, { diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 04f38c07..0ee0c646 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -31,6 +31,10 @@ export default { 'loading...': 'lädt...', 'Loading...': 'Lade...', 'no more notes': 'keine weiteren Notizen', + 'calendar entries': 'Kalender-Einträge', + 'Loading calendar events...': 'Kalender-Einträge werden geladen...', + 'No calendar events found': 'Keine Kalender-Einträge gefunden', + 'Calendar events in the next {{count}} months': 'Kalender-Einträge in den nächsten {{count}} Monaten', 'reply to': 'antworten an', reply: 'antworten', Reply: 'Antwort', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 2f2637b5..f229e64f 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -30,6 +30,10 @@ export default { 'loading...': 'loading...', 'Loading...': 'Loading...', 'no more notes': 'no more notes', + 'calendar entries': 'calendar entries', + 'Loading calendar events...': 'Loading calendar events...', + 'No calendar events found': 'No calendar events found', + 'Calendar events in the next {{count}} months': 'Calendar events in the next {{count}} months', 'The nostr.band relay appears to be temporarily out of service. Please try again later.': 'The nostr.band relay appears to be temporarily out of service. Please try again later.', 'reply to': 'reply to', reply: 'reply', diff --git a/src/pages/secondary/NotePage/index.tsx b/src/pages/secondary/NotePage/index.tsx index d5189c0b..c8f556be 100644 --- a/src/pages/secondary/NotePage/index.tsx +++ b/src/pages/secondary/NotePage/index.tsx @@ -17,11 +17,13 @@ import { tagNameEquals } from '@/lib/tag' import { cn } from '@/lib/utils' import { Ellipsis } from 'lucide-react' import type { Event } from 'nostr-tools' -import { kinds } from 'nostr-tools' +import { kinds, nip19 } from 'nostr-tools' import { forwardRef, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import NotFound from './NotFound' +const NADDR_REGEX = /nostr:(naddr1[a-z0-9]+)/g + // Helper function to get event type name (matching WebPreview) function getEventTypeName(kind: number): string { switch (kind) { @@ -102,7 +104,26 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string; ) const { isFetching: isFetchingRootEvent, event: rootEvent } = useFetchEvent(rootEventId) const { isFetching: isFetchingParentEvent, event: parentEvent } = useFetchEvent(parentEventId) - + + // When viewing a kind-24 invite (e.g. from notifications), extract calendar event naddr from content and show full calendar card with RSVP + const calendarInviteNaddr = useMemo(() => { + if (finalEvent?.kind !== ExtendedKind.PUBLIC_MESSAGE || !finalEvent.content?.trim()) return undefined + const match = NADDR_REGEX.exec(finalEvent.content) + NADDR_REGEX.lastIndex = 0 + const naddr = match?.[1] + if (!naddr) return undefined + try { + const decoded = nip19.decode(naddr) + if (decoded.type === 'naddr' && (decoded.data.kind === ExtendedKind.CALENDAR_EVENT_DATE || decoded.data.kind === ExtendedKind.CALENDAR_EVENT_TIME)) { + return naddr + } + } catch { + // ignore decode errors + } + return undefined + }, [finalEvent?.kind, finalEvent?.content]) + const { event: calendarInviteEvent } = useFetchEvent(calendarInviteNaddr) + // Fetch profile for author (for OpenGraph metadata) const { profile: authorProfile } = useFetchProfile(finalEvent?.pubkey) @@ -465,6 +486,11 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string; hideParentNotePreview originalNoteId={id} showFull + fullCalendarInvite={ + calendarInviteEvent && calendarInviteNaddr + ? { event: calendarInviteEvent, naddr: calendarInviteNaddr } + : undefined + } /> diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 7576e5fb..eadcedad 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -1272,15 +1272,9 @@ class ClientService extends EventTarget { resolveWithEvents() }, 1000) // Wait 1 second for more events } - } else { - // No events and no EOSE - connection closed early - // Wait a bit to see if events arrive, but not too long - if (!resolveTimeout) { - resolveTimeout = setTimeout(() => { - resolveWithEvents() - }, 2000) // Wait 2 seconds for events - } } + // No events yet and this relay closed (e.g. blocked/failed). Do NOT set a short + // timeout: other relays may still deliver. Let EOSE or globalTimeout resolve. } })