6 changed files with 24 additions and 352 deletions
@ -1,65 +0,0 @@
@@ -1,65 +0,0 @@
|
||||
import { stableSpellFeedFilterKey } from '@/lib/spell-feed-request-identity' |
||||
import type { TFeedSubRequest } from '@/types' |
||||
import { normalizeUrl, subtractNormalizedRelayUrls } from '@/lib/url' |
||||
import type { Filter } from 'nostr-tools' |
||||
|
||||
function normalizedRelayUrlSet(requests: TFeedSubRequest[]): Set<string> { |
||||
const s = new Set<string>() |
||||
for (const r of requests) { |
||||
for (const u of r.urls) { |
||||
const n = normalizeUrl(u) || u.trim() |
||||
if (n) s.add(n) |
||||
} |
||||
} |
||||
return s |
||||
} |
||||
|
||||
function dedupeShardKey(urls: string[], filter: Filter): string { |
||||
const nu = [...urls].map((u) => normalizeUrl(u) || u).filter(Boolean).sort() |
||||
return `${nu.join('\0')}|${stableSpellFeedFilterKey(filter)}` |
||||
} |
||||
|
||||
/** |
||||
* Second-wave REQ shards for the home following feed: relays and/or author groups not covered by the |
||||
* provisional (kind-3 tags) subscription. Keeps the first subscription open and avoids "closed by caller" churn. |
||||
*/ |
||||
export function buildFollowingFeedDeltaSubRequests( |
||||
fullAugmented: TFeedSubRequest[], |
||||
provisionalAugmented: TFeedSubRequest[], |
||||
provisionalAuthorHexes: string[] |
||||
): TFeedSubRequest[] { |
||||
if (fullAugmented.length === 0) return [] |
||||
|
||||
const rProv = normalizedRelayUrlSet(provisionalAugmented) |
||||
const rProvList = [...rProv] |
||||
const aProv = new Set(provisionalAuthorHexes.map((p) => p.toLowerCase())) |
||||
|
||||
const out: TFeedSubRequest[] = [] |
||||
const seen = new Set<string>() |
||||
|
||||
for (const req of fullAugmented) { |
||||
const filter = req.filter as Filter |
||||
const authorsRaw = Array.isArray(filter.authors) ? filter.authors : [] |
||||
const authors = authorsRaw.map((x) => (typeof x === 'string' ? x.toLowerCase() : x)) as string[] |
||||
|
||||
const uDelta = subtractNormalizedRelayUrls(req.urls, rProvList) |
||||
const authorsNew = authors.filter((a) => typeof a === 'string' && a.length === 64 && !aProv.has(a)) |
||||
|
||||
const pushIfNew = (urls: string[], f: Filter) => { |
||||
if (urls.length === 0) return |
||||
const k = dedupeShardKey(urls, f) |
||||
if (seen.has(k)) return |
||||
seen.add(k) |
||||
out.push({ ...req, urls, filter: f }) |
||||
} |
||||
|
||||
if (uDelta.length > 0) { |
||||
pushIfNew(uDelta, { ...filter, authors } as Filter) |
||||
} |
||||
if (authorsNew.length > 0) { |
||||
pushIfNew(req.urls, { ...filter, authors: authorsNew } as Filter) |
||||
} |
||||
} |
||||
|
||||
return out |
||||
} |
||||
@ -1,199 +0,0 @@
@@ -1,199 +0,0 @@
|
||||
import NormalFeed from '@/components/NormalFeed' |
||||
import type { TNoteListRef } from '@/components/NoteList' |
||||
import { |
||||
augmentSubRequestsWithFavoritesFastReadAndInbox, |
||||
userReadRelaysWithHttp |
||||
} from '@/lib/favorites-feed-relays' |
||||
import { buildFollowingFeedDeltaSubRequests } from '@/lib/following-feed-delta' |
||||
import { getPubkeysFromPTags } from '@/lib/tag' |
||||
import { normalizeUrl } from '@/lib/url' |
||||
import logger from '@/lib/logger' |
||||
import { useFeed } from '@/providers/FeedProvider' |
||||
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' |
||||
import { useNostr } from '@/providers/NostrProvider' |
||||
import { |
||||
buildWispTrendingNotesRelayUrl, |
||||
WISP_TRENDING_FEED_KINDS |
||||
} from '@/lib/wisp-trending-relay' |
||||
import client from '@/services/client.service' |
||||
import { TFeedSubRequest } from '@/types' |
||||
import type { ReactNode } from 'react' |
||||
import { forwardRef, useEffect, useMemo, useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
|
||||
const FollowingFeed = forwardRef< |
||||
TNoteListRef, |
||||
{ |
||||
setSubHeader?: (node: ReactNode) => void |
||||
onSubHeaderRefresh?: () => void |
||||
} |
||||
>(function FollowingFeed({ setSubHeader, onSubHeaderRefresh }, ref) { |
||||
const { t, i18n } = useTranslation() |
||||
const { pubkey, relayList, followListEvent } = useNostr() |
||||
const { favoriteRelays, blockedRelays } = useFavoriteRelays() |
||||
const { feedInfo } = useFeed() |
||||
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([]) |
||||
const [deltaSubRequests, setDeltaSubRequests] = useState<TFeedSubRequest[]>([]) |
||||
|
||||
const favoriteRelaysKey = useMemo( |
||||
() => |
||||
[...favoriteRelays] |
||||
.map((u) => normalizeUrl(u) || u) |
||||
.filter(Boolean) |
||||
.sort() |
||||
.join('\0'), |
||||
[favoriteRelays] |
||||
) |
||||
const blockedRelaysKey = useMemo( |
||||
() => |
||||
[...blockedRelays] |
||||
.map((u) => normalizeUrl(u) || u) |
||||
.filter(Boolean) |
||||
.sort() |
||||
.join('\0'), |
||||
[blockedRelays] |
||||
) |
||||
const relayReadKey = useMemo( |
||||
() => |
||||
[...userReadRelaysWithHttp(relayList)] |
||||
.map((u) => normalizeUrl(u) || u) |
||||
.filter(Boolean) |
||||
.sort() |
||||
.join('\0'), |
||||
[relayList] |
||||
) |
||||
const relayWriteKey = useMemo( |
||||
() => |
||||
[...(relayList?.write ?? [])] |
||||
.map((u) => normalizeUrl(u) || u) |
||||
.filter(Boolean) |
||||
.sort() |
||||
.join('\0'), |
||||
[relayList?.write] |
||||
) |
||||
|
||||
const followingFeedSubscriptionKey = useMemo( |
||||
() => (pubkey ? `home-following:${pubkey.toLowerCase()}` : undefined), |
||||
[pubkey] |
||||
) |
||||
|
||||
useEffect(() => { |
||||
let cancelled = false |
||||
async function init() { |
||||
if (feedInfo.feedType !== 'following' || !pubkey) { |
||||
setSubRequests([]) |
||||
setDeltaSubRequests([]) |
||||
return |
||||
} |
||||
|
||||
setDeltaSubRequests([]) |
||||
|
||||
const augment = (raw: TFeedSubRequest[]) => |
||||
augmentSubRequestsWithFavoritesFastReadAndInbox( |
||||
raw, |
||||
favoriteRelays, |
||||
blockedRelays, |
||||
userReadRelaysWithHttp(relayList), |
||||
{ userWriteRelays: relayList?.write ?? [] } |
||||
) |
||||
|
||||
const trendingRelayUrl = buildWispTrendingNotesRelayUrl() |
||||
const wispTrendingShard: TFeedSubRequest = { |
||||
urls: [trendingRelayUrl], |
||||
filter: { kinds: [...WISP_TRENDING_FEED_KINDS], limit: 100 }, |
||||
reasonLabel: t('Trending on Nostr'), |
||||
reasonLabelIfSeenOnRelay: trendingRelayUrl |
||||
} |
||||
const appendTrending = (batch: TFeedSubRequest[]) => [...batch, wispTrendingShard] |
||||
|
||||
const fromTags = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : [] |
||||
const provisionalAuthors = [...new Set([pubkey, ...fromTags])] |
||||
const provisionalAuthorLower = provisionalAuthors.map((p) => p.toLowerCase()) |
||||
|
||||
let rawProv: TFeedSubRequest[] = [] |
||||
try { |
||||
rawProv = await client.generateSubRequestsForPubkeys(provisionalAuthors, pubkey) |
||||
} catch (error) { |
||||
logger.warn('[FollowingFeed] provisional generateSubRequestsForPubkeys failed', { error }) |
||||
} |
||||
const provAugCore = augment(rawProv) |
||||
const provAug = appendTrending(provAugCore) |
||||
if (!cancelled) setSubRequests(provAug) |
||||
|
||||
let followings: string[] = fromTags |
||||
try { |
||||
followings = await client.fetchFollowings(pubkey) |
||||
} catch (error) { |
||||
followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : [] |
||||
logger.warn('[FollowingFeed] fetchFollowings failed; using cached follow list fallback', { |
||||
error, |
||||
fallbackCount: followings.length |
||||
}) |
||||
} |
||||
|
||||
const fullAuthors = [...new Set([pubkey, ...followings])] |
||||
|
||||
try { |
||||
const rawFull = await client.generateSubRequestsForPubkeys(fullAuthors, pubkey) |
||||
if (cancelled) return |
||||
const fullAugCore = augment(rawFull) |
||||
const delta = buildFollowingFeedDeltaSubRequests(fullAugCore, provAugCore, provisionalAuthorLower) |
||||
if (!cancelled) { |
||||
setDeltaSubRequests(delta) |
||||
if (delta.length > 0) { |
||||
logger.info('[FollowingFeed] delta wave subRequests', { |
||||
deltaShardCount: delta.length, |
||||
provisionalShardCount: provAugCore.length, |
||||
fullShardCount: fullAugCore.length |
||||
}) |
||||
} |
||||
} |
||||
} catch (error) { |
||||
logger.error('[FollowingFeed] full generateSubRequestsForPubkeys failed', error) |
||||
if (!cancelled) setDeltaSubRequests([]) |
||||
} |
||||
} |
||||
|
||||
void init() |
||||
return () => { |
||||
cancelled = true |
||||
} |
||||
}, [ |
||||
feedInfo.feedType, |
||||
pubkey, |
||||
followListEvent?.id, |
||||
favoriteRelaysKey, |
||||
blockedRelaysKey, |
||||
relayReadKey, |
||||
relayWriteKey, |
||||
i18n.language |
||||
]) |
||||
|
||||
const trendingFeedNotice = useMemo( |
||||
() => ( |
||||
<p className="mb-2 px-1 text-xs text-muted-foreground leading-snug"> |
||||
{t('Home trending slice notice')} |
||||
</p> |
||||
), |
||||
[t] |
||||
) |
||||
|
||||
return ( |
||||
<NormalFeed |
||||
ref={ref} |
||||
subRequests={subRequests} |
||||
followingFeedDeltaSubRequests={deltaSubRequests} |
||||
feedSubscriptionKey={followingFeedSubscriptionKey} |
||||
preserveTimelineOnSubRequestsChange |
||||
isMainFeed |
||||
setSubHeader={setSubHeader} |
||||
onSubHeaderRefresh={onSubHeaderRefresh} |
||||
showFeedClientFilter={false} |
||||
hostPrimaryPageName="feed" |
||||
feedTopNotice={trendingFeedNotice} |
||||
/> |
||||
) |
||||
}) |
||||
|
||||
FollowingFeed.displayName = 'FollowingFeed' |
||||
export default FollowingFeed |
||||
Loading…
Reference in new issue