28 changed files with 515 additions and 359 deletions
@ -1,35 +0,0 @@ |
|||||||
import RssFeedItem from '@/components/RssFeedItem' |
|
||||||
import type { RssFeedItem as TRssFeedItem } from '@/services/rss-feed.service' |
|
||||||
import { useTranslation } from 'react-i18next' |
|
||||||
|
|
||||||
/** Classic RSS reader: one row per feed item, chronological. */ |
|
||||||
export function RssEntriesSection({ items }: { items: TRssFeedItem[] }) { |
|
||||||
const { t } = useTranslation() |
|
||||||
if (items.length === 0) return null |
|
||||||
return ( |
|
||||||
<section className="space-y-3" aria-labelledby="jumble-rss-entries-heading"> |
|
||||||
<div className="space-y-1 px-0.5"> |
|
||||||
<h2 |
|
||||||
id="jumble-rss-entries-heading" |
|
||||||
className="text-xs font-semibold uppercase tracking-wide text-muted-foreground" |
|
||||||
> |
|
||||||
{t('RSS timeline')} |
|
||||||
</h2> |
|
||||||
<p className="text-[11px] leading-snug text-muted-foreground/90"> |
|
||||||
{t('RSS timeline subtitle')} |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
<div className="space-y-0 divide-y divide-border overflow-hidden rounded-xl border border-border bg-card"> |
|
||||||
{items.map((item) => ( |
|
||||||
<RssFeedItem |
|
||||||
key={`${item.feedUrl}-${item.guid}`} |
|
||||||
item={item} |
|
||||||
layout="list" |
|
||||||
sourceStrip="rss" |
|
||||||
className="rounded-none border-0 bg-transparent shadow-none" |
|
||||||
/> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</section> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -0,0 +1,23 @@ |
|||||||
|
import type { ReactNode } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
|
||||||
|
/** Section chrome for the RSS column: feed items and article cards backed by subscribed feeds. */ |
||||||
|
export function RssUnifiedScopeSection({ children }: { children: ReactNode }) { |
||||||
|
const { t } = useTranslation() |
||||||
|
return ( |
||||||
|
<section className="space-y-3" aria-labelledby="jumble-rss-unified-heading"> |
||||||
|
<div className="space-y-1 px-0.5"> |
||||||
|
<h2 |
||||||
|
id="jumble-rss-unified-heading" |
||||||
|
className="text-xs font-semibold uppercase tracking-wide text-muted-foreground" |
||||||
|
> |
||||||
|
{t('RSS feed column title')} |
||||||
|
</h2> |
||||||
|
<p className="text-[11px] leading-snug text-muted-foreground/90"> |
||||||
|
{t('RSS feed column subtitle')} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div className="space-y-4">{children}</div> |
||||||
|
</section> |
||||||
|
) |
||||||
|
} |
||||||
@ -1,98 +0,0 @@ |
|||||||
import NoteCard from '@/components/NoteCard' |
|
||||||
import { Skeleton } from '@/components/ui/skeleton' |
|
||||||
import { useRssUrlThreadQueryRelays } from '@/hooks/useRssUrlThreadQueryRelays' |
|
||||||
import { |
|
||||||
buildRssArticleUrlThreadInteractionFilterGroups, |
|
||||||
isRssArticleUrlThreadInteraction |
|
||||||
} from '@/lib/rss-web-feed' |
|
||||||
import { queryService } from '@/services/client.service' |
|
||||||
import type { Event } from 'nostr-tools' |
|
||||||
import { useEffect, useState } from 'react' |
|
||||||
|
|
||||||
const PREVIEW_LIMIT = 5 |
|
||||||
const FETCH_LIMIT = 24 |
|
||||||
|
|
||||||
/** |
|
||||||
* Compact Nostr thread rows (comments + highlights) for an article URL card in the RSS+Web feed. |
|
||||||
*/ |
|
||||||
export default function RssUrlThreadEventsPreview({ canonicalUrl }: { canonicalUrl: string }) { |
|
||||||
const { relayUrls, key: relayKey } = useRssUrlThreadQueryRelays() |
|
||||||
const [events, setEvents] = useState<Event[]>([]) |
|
||||||
const [loading, setLoading] = useState(true) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
let cancelled = false |
|
||||||
setLoading(true) |
|
||||||
const { nonSocial, social } = buildRssArticleUrlThreadInteractionFilterGroups( |
|
||||||
canonicalUrl, |
|
||||||
FETCH_LIMIT |
|
||||||
) |
|
||||||
const fetchOpts = { |
|
||||||
eoseTimeout: 12_000, |
|
||||||
globalTimeout: 26_000, |
|
||||||
firstRelayResultGraceMs: false as const |
|
||||||
} |
|
||||||
if (relayUrls.length === 0) { |
|
||||||
return () => { |
|
||||||
cancelled = true |
|
||||||
} |
|
||||||
} |
|
||||||
void Promise.all([ |
|
||||||
nonSocial.length > 0 ? queryService.fetchEvents(relayUrls, nonSocial, fetchOpts) : Promise.resolve([]), |
|
||||||
social.length > 0 ? queryService.fetchEvents(relayUrls, social, fetchOpts) : Promise.resolve([]) |
|
||||||
]) |
|
||||||
.then(([a, b]) => { |
|
||||||
if (cancelled) return |
|
||||||
const all = [...a, ...b] |
|
||||||
const seen = new Set<string>() |
|
||||||
const merged: Event[] = [] |
|
||||||
for (const e of [...all].sort((x, y) => y.created_at - x.created_at)) { |
|
||||||
if (seen.has(e.id)) continue |
|
||||||
if (!isRssArticleUrlThreadInteraction(e, canonicalUrl)) continue |
|
||||||
seen.add(e.id) |
|
||||||
merged.push(e) |
|
||||||
} |
|
||||||
setEvents(merged.slice(0, PREVIEW_LIMIT)) |
|
||||||
}) |
|
||||||
.catch(() => { |
|
||||||
if (!cancelled) setEvents([]) |
|
||||||
}) |
|
||||||
.finally(() => { |
|
||||||
if (!cancelled) setLoading(false) |
|
||||||
}) |
|
||||||
return () => { |
|
||||||
cancelled = true |
|
||||||
} |
|
||||||
}, [canonicalUrl, relayKey, relayUrls]) |
|
||||||
|
|
||||||
if (loading) { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="border-t border-border/50 bg-muted/10 px-3 py-2 pointer-events-auto space-y-2" |
|
||||||
onClick={(e) => e.stopPropagation()} |
|
||||||
onKeyDown={(e) => e.stopPropagation()} |
|
||||||
> |
|
||||||
<Skeleton className="h-14 w-full rounded-md" /> |
|
||||||
<Skeleton className="h-14 w-full rounded-md" /> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
if (events.length === 0) return null |
|
||||||
|
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="border-t border-border/50 bg-muted/10 pointer-events-auto max-h-72 overflow-y-auto" |
|
||||||
onClick={(e) => e.stopPropagation()} |
|
||||||
onKeyDown={(e) => e.stopPropagation()} |
|
||||||
> |
|
||||||
<div className="divide-y divide-border/40"> |
|
||||||
{events.map((evt) => ( |
|
||||||
<div key={evt.id} className="px-2 py-1.5"> |
|
||||||
<NoteCard event={evt} className="border-0 bg-transparent shadow-none" hideParentNotePreview /> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
Loading…
Reference in new issue