You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

186 lines
6.1 KiB

/**
* Built-in “faux spells” use the same NoteList path as kind-777 REQ spells.
*/
import {
DEFAULT_FAVORITE_RELAYS,
ExtendedKind,
FAST_READ_RELAY_URLS,
FAST_WRITE_RELAY_URLS,
PROFILE_FEED_KINDS
} from '@/constants'
import { normalizeTopic } from '@/lib/discussion-topics'
import { normalizeUrl } from '@/lib/url'
import type { TFeedSubRequest, TRelayList } from '@/types'
import { type Event, type Filter } from 'nostr-tools'
const NOTIFICATION_LIMIT = 500
const DISCUSSION_LIMIT = 500
const MAX_BOOKMARK_IDS = 250
/**
* Spells “Discussions” uses NoteList → subscribeTimeline → one live REQ per relay.
* The same merged list as DiscussionsPage’s one-shot query would open 80+ sockets and exhaust
* subscription slots; cap keeps first paint fast. Full coverage remains on /discussions.
*/
const DISCUSSION_FAUX_SPELL_MAX_RELAYS = 32
export const MEDIA_SPELL_KINDS = [
ExtendedKind.PICTURE,
ExtendedKind.VIDEO,
ExtendedKind.SHORT_VIDEO,
ExtendedKind.VOICE
] as const
/** Relays for “global” faux feeds (media, calendar): visible favorites or defaults. */
export function fauxFavoriteRelayUrls(favoriteRelays: string[], blockedRelays: string[]): string[] {
const blocked = new Set(blockedRelays.map((b) => normalizeUrl(b) || b))
const visible = favoriteRelays.filter((r) => {
const k = normalizeUrl(r) || r
return k && !blocked.has(k)
})
const base = visible.length > 0 ? visible : DEFAULT_FAVORITE_RELAYS
return dedupe(base.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[])
}
/** Same cap/priority as the main Notification list: read/inbox relays first, then favorites, then defaults (few relays → faster EOSE, fewer dead sockets). */
const NOTIFICATION_FEED_MAX_RELAYS = 5
function relayUrlsUpToUnblocked(urls: string[], blocked: Set<string>, max: number): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const u of urls) {
const k = normalizeUrl(u) || u
if (!k || blocked.has(k) || seen.has(k)) continue
seen.add(k)
out.push(k)
if (out.length >= max) break
}
return out
}
export function notificationRelayUrls(
relayList: TRelayList | null | undefined,
favoriteRelays: string[],
blockedRelays: string[] = []
): string[] {
const blocked = new Set(blockedRelays.map((b) => normalizeUrl(b) || b))
const read = relayList?.read ?? []
if (read.length > 0) {
const fromRead = relayUrlsUpToUnblocked(read, blocked, NOTIFICATION_FEED_MAX_RELAYS)
if (fromRead.length > 0) return fromRead
}
if (favoriteRelays.length > 0) {
const fromFav = relayUrlsUpToUnblocked(favoriteRelays, blocked, NOTIFICATION_FEED_MAX_RELAYS)
if (fromFav.length > 0) return fromFav
}
return relayUrlsUpToUnblocked(FAST_READ_RELAY_URLS, blocked, NOTIFICATION_FEED_MAX_RELAYS)
}
function dedupe(urls: string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const u of urls) {
const k = normalizeUrl(u) || u
if (!k || seen.has(k)) continue
seen.add(k)
out.push(k)
}
return out
}
/** Notifications spell: same kind set as profile-style feeds, restricted to `#p` = you on the relay. */
export function buildMentionsSpellFilter(pubkey: string): Filter {
return {
kinds: [...PROFILE_FEED_KINDS],
limit: NOTIFICATION_LIMIT,
'#p': [pubkey]
}
}
/**
* Relay set for Spells “Discussions” (kind 11): same merge order as DiscussionsPage, but capped
* for subscription-based loading (see DISCUSSION_FAUX_SPELL_MAX_RELAYS).
*/
export function discussionRelayUrls(
relayList: TRelayList | null | undefined,
favoriteRelays: string[],
blockedRelays: string[]
): string[] {
const read = relayList?.read ?? []
const write = relayList?.write ?? []
const merged = [...read, ...write, ...favoriteRelays, ...FAST_READ_RELAY_URLS, ...FAST_WRITE_RELAY_URLS]
const blocked = new Set(blockedRelays.map((b) => normalizeUrl(b) || b))
const seen = new Set<string>()
const out: string[] = []
for (const u of merged) {
const k = normalizeUrl(u) || u
if (!k || seen.has(k) || blocked.has(k)) continue
seen.add(k)
out.push(k)
if (out.length >= DISCUSSION_FAUX_SPELL_MAX_RELAYS) break
}
return out
}
export function buildDiscussionFilter(): Filter {
return {
kinds: [ExtendedKind.DISCUSSION],
limit: DISCUSSION_LIMIT
}
}
export function buildMediaSpellFilter(): Filter {
return { kinds: [...MEDIA_SPELL_KINDS], limit: 500 }
}
export function buildCalendarSpellFilter(): Filter {
return {
kinds: [ExtendedKind.CALENDAR_EVENT_DATE, ExtendedKind.CALENDAR_EVENT_TIME],
limit: 200
}
}
const FOLLOW_PACK_LIMIT = 100
/** Kind 39089 follow/starter packs from fast read relays (same scope as the old Follow Packs page). */
export function buildFollowPacksSubRequests(): TFeedSubRequest[] {
const urls = FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[]
if (!urls.length) return []
return [
{
urls,
filter: { kinds: [ExtendedKind.FOLLOW_PACK], limit: FOLLOW_PACK_LIMIT }
}
]
}
/** One subrequest per topic (OR). Uses same kind set as the main profile/favorites feed. */
export function buildInterestsSubRequests(
relayUrls: string[],
rawTopics: string[],
kindsList: number[] = PROFILE_FEED_KINDS
): TFeedSubRequest[] {
if (!relayUrls.length || !rawTopics.length || !kindsList.length) return []
const topics = Array.from(
new Set(rawTopics.map((t) => normalizeTopic(t)).filter((t) => t.length > 0))
)
if (!topics.length) return []
return topics.map((topic) => ({
urls: relayUrls,
filter: {
kinds: kindsList,
'#t': [topic],
limit: 400
}
}))
}
/** Bookmark list e-tags only (hex ids); addressable (a-tag) bookmarks need separate fetches. */
export function buildBookmarksSubRequests(bookmarkListEvent: Event | null, urls: string[]): TFeedSubRequest[] {
if (!bookmarkListEvent?.tags?.length || !urls.length) return []
const ids = bookmarkListEvent.tags
.filter((t) => t[0] === 'e' && t[1] && /^[a-f0-9]{64}$/i.test(t[1]))
.map((t) => t[1] as string)
if (!ids.length) return []
return [{ urls, filter: { ids: ids.slice(0, MAX_BOOKMARK_IDS), limit: MAX_BOOKMARK_IDS } }]
}