Browse Source

bug-fix build

imwald
Silberengel 1 month ago
parent
commit
5733c52d64
  1. 6
      src/PageManager.tsx
  2. 1
      src/components/Embedded/EmbeddedCalendarEvent.tsx
  3. 36
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  4. 14
      src/components/Note/index.tsx
  5. 201
      src/components/TrendingNotes/index.tsx
  6. 42
      src/hooks/useFetchCalendarRsvps.tsx
  7. 19
      src/hooks/useProfileTimeline.tsx
  8. 4
      src/i18n/locales/de.ts
  9. 4
      src/i18n/locales/en.ts
  10. 30
      src/pages/secondary/NotePage/index.tsx
  11. 10
      src/services/client.service.ts

6
src/PageManager.tsx

@ -578,7 +578,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -578,7 +578,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
const savedFeedStateRef = useRef<Map<TPrimaryPageName, {
tab?: string,
discussionsState?: { selectedTopic: string, timeSpan: '30days' | '90days' | 'all' },
trendingTab?: 'relays' | 'hashtags'
trendingTab?: 'relays' | 'hashtags' | 'calendar'
}>>(new Map())
const currentTabStateRef = useRef<Map<TPrimaryPageName, string>>(new Map()) // Track current tab state for each page
@ -610,7 +610,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -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 }) { @@ -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', {

1
src/components/Embedded/EmbeddedCalendarEvent.tsx

@ -1,4 +1,3 @@ @@ -1,4 +1,3 @@
import { ExtendedKind } from '@/constants'
import {
getCalendarEventMeta,
formatCalendarTime,

36
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -20,6 +20,7 @@ import React, { useMemo, useState, useCallback, useEffect, useRef } from 'react' @@ -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( @@ -422,9 +423,11 @@ function parseMarkdownContent(
imageThumbnailMap?: Map<string, string>
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<string>; footnotes: Map<string, string>; 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<string>()
const footnotes = new Map<string, string>()
@ -2154,12 +2157,21 @@ function parseMarkdownContent( @@ -2154,12 +2157,21 @@ function parseMarkdownContent(
</span>
)
} else if (bech32Id.startsWith('note') || bech32Id.startsWith('nevent') || bech32Id.startsWith('naddr')) {
// Embedded events should be block-level and fill width
parts.push(
<div key={`nostr-${patternIdx}`} className="w-full my-2">
<EmbeddedNote noteId={bech32Id} />
</div>
)
// When this is the calendar invite naddr, show full calendar card with RSVP instead of embedded preview
if (fullCalendarInvite && fullCalendarInvite.naddr === bech32Id) {
parts.push(
<div key={`nostr-${patternIdx}`} className="w-full my-2">
<CalendarEventContent event={fullCalendarInvite.event} className="mt-2" showRsvp />
</div>
)
} else {
// Embedded events should be block-level and fill width
parts.push(
<div key={`nostr-${patternIdx}`} className="w-full my-2">
<EmbeddedNote noteId={bech32Id} />
</div>
)
}
} else {
parts.push(<span key={`nostr-${patternIdx}`}>nostr:{bech32Id}</span>)
}
@ -3158,12 +3170,15 @@ export default function MarkdownArticle({ @@ -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({ @@ -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(() => {

14
src/components/Note/index.tsx

@ -51,7 +51,8 @@ export default function Note({ @@ -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({ @@ -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({ @@ -220,7 +223,14 @@ export default function Note({
} else if (event.kind === ExtendedKind.CALENDAR_EVENT_TIME || event.kind === ExtendedKind.CALENDAR_EVENT_DATE) {
content = <CalendarEventContent event={event} className="mt-2" showRsvp />
} else if (event.kind === ExtendedKind.PUBLIC_MESSAGE) {
content = <MarkdownArticle className="mt-2" event={event} hideMetadata={true} />
content = (
<MarkdownArticle
className="mt-2"
event={event}
hideMetadata={true}
fullCalendarInvite={fullCalendarInvite}
/>
)
} else if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) {
content = <Zap className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.FOLLOW_PACK) {

201
src/components/TrendingNotes/index.tsx

@ -1,4 +1,5 @@ @@ -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' @@ -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: { @@ -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() { @@ -46,12 +83,14 @@ export default function TrendingNotes() {
const [popularHashtags, setPopularHashtags] = useState<string[]>([])
const [cacheEvents, setCacheEvents] = useState<NostrEvent[]>([])
const [cacheLoading, setCacheLoading] = useState(false)
const [calendarEvents, setCalendarEvents] = useState<NostrEvent[]>([])
const [calendarLoading, setCalendarLoading] = useState(false)
const bottomRef = useRef<HTMLDivElement>(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() { @@ -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<string>([
...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<string>()
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<string>()
@ -467,9 +566,25 @@ export default function TrendingNotes() { @@ -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<string, NostrEvent[]>()
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() { @@ -562,10 +677,25 @@ export default function TrendingNotes() {
>
hashtags
</button>
<button
onClick={() => {
setActiveTab('calendar')
window.dispatchEvent(new CustomEvent('pageTabChanged', {
detail: { page: 'search', tab: 'calendar' }
}))
}}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'calendar'
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`}
>
{t('calendar entries')}
</button>
</div>
</div>
{/* Second row controls for tabs 2-3 */}
{/* Second row controls for relays / hashtags (calendar has no sort – ordered by datetime) */}
{(activeTab === 'relays' || activeTab === 'hashtags') && (
<div className="flex items-center gap-4 px-4 pb-2">
{/* Sorting controls - not shown for hashtags tab */}
@ -652,17 +782,59 @@ export default function TrendingNotes() { @@ -652,17 +782,59 @@ export default function TrendingNotes() {
Loading trending notes from your relays...
</div>
)}
{/* Show loading message for calendar tab */}
{activeTab === 'calendar' && calendarLoading && calendarEvents.length === 0 && (
<div className="text-center text-sm text-muted-foreground mt-8">
{t('Loading calendar events...')}
</div>
)}
{activeTab === 'calendar' && !calendarLoading && calendarEvents.length === 0 && (
<div className="text-center text-sm text-muted-foreground mt-8">
{t('No calendar events found')}
</div>
)}
{filteredEvents.map((event) => (
<NoteCard key={event.id} className="w-full" event={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 (
<div key={monthKey} className="mt-6 first:mt-0">
<h3 className="text-sm font-semibold text-muted-foreground px-4 py-2 border-b bg-muted/30">
{monthLabel}
</h3>
<div className="space-y-0">
{eventsInMonth.map((event) => (
<NoteCard
key={isReplaceableEvent((event as any).kind) ? getReplaceableCoordinateFromEvent(event as any) : (event as any).id}
className="w-full"
event={event}
/>
))}
</div>
</div>
)
})
: filteredEvents.map((event) => (
<NoteCard
key={isReplaceableEvent((event as any).kind) ? getReplaceableCoordinateFromEvent(event as any) : (event as any).id}
className="w-full"
event={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() { @@ -671,6 +843,13 @@ export default function TrendingNotes() {
</div>
)
}
if (calendarShowingAll && calendarEvents.length > 0) {
return (
<div className="text-center text-sm text-muted-foreground mt-4 pb-4">
{t('Calendar events in the next {{count}} months', { count: CALENDAR_MONTHS_AHEAD })}
</div>
)
}
return <div className="text-center text-sm text-muted-foreground mt-2">{t('no more notes')}</div>
})()}
</div>

42
src/hooks/useFetchCalendarRsvps.tsx

@ -39,22 +39,40 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -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<string>([
...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) { @@ -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(() => {

19
src/hooks/useProfileTimeline.tsx

@ -1,7 +1,7 @@ @@ -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({ @@ -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({ @@ -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, {

4
src/i18n/locales/de.ts

@ -31,6 +31,10 @@ export default { @@ -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',

4
src/i18n/locales/en.ts

@ -30,6 +30,10 @@ export default { @@ -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',

30
src/pages/secondary/NotePage/index.tsx

@ -17,11 +17,13 @@ import { tagNameEquals } from '@/lib/tag' @@ -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; @@ -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; @@ -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
}
/>
<NoteStats className="mt-3" event={finalEvent} fetchIfNotExisting displayTopZapsAndLikes />
</div>

10
src/services/client.service.ts

@ -1272,15 +1272,9 @@ class ClientService extends EventTarget { @@ -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.
}
})

Loading…
Cancel
Save