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
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 } }] |
|
}
|
|
|