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.
251 lines
9.0 KiB
251 lines
9.0 KiB
import { |
|
DEFAULT_FAVORITE_RELAYS, |
|
FAST_READ_RELAY_URLS, |
|
READ_ONLY_RELAY_URLS, |
|
relayFilterIncludesSocialKindBlockedKind |
|
} from '@/constants' |
|
import type { TFeedSubRequest } from '@/types' |
|
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url' |
|
import { |
|
buildPrioritizedReadRelayUrls, |
|
buildReadRelayPriorityLayers, |
|
dedupeNormalizeRelayUrlsOrdered, |
|
MAX_REQ_RELAY_URLS, |
|
mergeRelayPriorityLayers, |
|
relayUrlsLocalsFirst |
|
} from '@/lib/relay-url-priority' |
|
|
|
const blockedSet = (blockedRelays: string[]) => |
|
new Set(blockedRelays.map((b) => normalizeAnyRelayUrl(b) || b)) |
|
|
|
/** |
|
* Logged-in user’s favorite relays (kind 10012 `relay` tags via {@link useFavoriteRelays}, plus bootstrap defaults |
|
* when the event is missing): drop blocked, dedupe, normalize. If no non-blocked entries remain, use |
|
* {@link DEFAULT_FAVORITE_RELAYS}. Same list drives the favorites tier in REQ/publish prioritization and the |
|
* all-favorites home feed. |
|
*/ |
|
/** |
|
* NIP-65 `read` plus HTTP index inboxes (kind 10243) for feed REQ / query URL lists. |
|
*/ |
|
export function userReadRelaysWithHttp( |
|
relayList: { read?: string[]; httpRead?: string[] } | undefined | null |
|
): string[] { |
|
const http = relayList?.httpRead ?? [] |
|
const read = relayList?.read ?? [] |
|
return dedupeNormalizeRelayUrlsOrdered([...http, ...read]) |
|
} |
|
|
|
export function getFavoritesFeedRelayUrls( |
|
favoriteRelays: string[], |
|
blockedRelays: string[] |
|
): string[] { |
|
const blocked = blockedSet(blockedRelays) |
|
const visible = favoriteRelays.filter((r) => { |
|
const k = normalizeAnyRelayUrl(r) || r |
|
return k && !blocked.has(k) |
|
}) |
|
const base = visible.length > 0 ? visible : DEFAULT_FAVORITE_RELAYS |
|
const seen = new Set<string>() |
|
const out: string[] = [] |
|
for (const u of base) { |
|
const k = normalizeAnyRelayUrl(u) || u |
|
if (!k || seen.has(k)) continue |
|
seen.add(k) |
|
out.push(k) |
|
} |
|
return out |
|
} |
|
|
|
/** |
|
* Merge relay URL lists in order; first occurrence wins; drops blocked. |
|
*/ |
|
export function mergeRelayUrlLayers(layers: string[][], blockedRelays: string[]): string[] { |
|
const blocked = blockedSet(blockedRelays) |
|
const seen = new Set<string>() |
|
const out: string[] = [] |
|
for (const layer of layers) { |
|
for (const u of layer) { |
|
const k = normalizeAnyRelayUrl(u) || u |
|
if (!k || blocked.has(k) || seen.has(k)) continue |
|
seen.add(k) |
|
out.push(k) |
|
} |
|
} |
|
return out |
|
} |
|
|
|
/** |
|
* Viewed author’s NIP-65 read list (inboxes), then write list (outboxes), each with LAN/local URLs first; blocked |
|
* stripped. Used for profile pins + Medien before {@link buildProfileAugmentedReadRelayUrls}. |
|
*/ |
|
export function buildAuthorInboxOutboxRelayUrls( |
|
authorRelayList: { read: string[]; write: string[]; httpRead?: string[]; httpWrite?: string[] }, |
|
blockedRelays: string[] |
|
): string[] { |
|
const inboxLayer = relayUrlsLocalsFirst([ |
|
...(authorRelayList.httpRead ?? []), |
|
...(authorRelayList.read ?? []) |
|
]) |
|
const outboxLayer = relayUrlsLocalsFirst([ |
|
...(authorRelayList.httpWrite ?? []), |
|
...(authorRelayList.write ?? []) |
|
]) |
|
return mergeRelayUrlLayers([inboxLayer, outboxLayer], blockedRelays) |
|
} |
|
|
|
/** |
|
* Profile pins + Medien: author NIP-65 tier (pass from {@link buildAuthorInboxOutboxRelayUrls}), then |
|
* {@link READ_ONLY_RELAY_URLS}, then {@link FAST_READ_RELAY_URLS}; dedupe, blocked-stripped, capped. |
|
*/ |
|
const PROFILE_AUGMENTED_READ_MAX_RELAYS = 16 |
|
|
|
export function buildProfileAugmentedReadRelayUrls( |
|
authorRelayUrls: string[], |
|
blockedRelays: string[], |
|
maxRelays: number = PROFILE_AUGMENTED_READ_MAX_RELAYS |
|
): string[] { |
|
const readOnlyLayer = READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) |
|
const fastReadLayer = FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) |
|
const merged = mergeRelayUrlLayers([authorRelayUrls, readOnlyLayer, fastReadLayer], blockedRelays) |
|
return merged.slice(0, maxRelays) |
|
} |
|
|
|
export type ReadRelayPriorityOptions = { |
|
/** User NIP-65 write list — local URLs are promoted with inboxes for REQ. */ |
|
userWriteRelays?: string[] |
|
/** Profile/timeline author outboxes (write relays) when known. */ |
|
authorWriteRelays?: string[] |
|
maxRelays?: number |
|
/** |
|
* When set, applies to all subrequests. When unset, each subrequest uses |
|
* {@link relayFilterIncludesSocialKindBlockedKind} on its filter to decide whether to strip |
|
* relays in `SOCIAL_KIND_BLOCKED_RELAY_URLS` before capping. |
|
*/ |
|
applySocialKindBlockedFilter?: boolean |
|
/** |
|
* When false, ignore each subrequest’s `urls` and use only the shared prioritized stack (rare). |
|
* Default true. |
|
*/ |
|
mergeSubrequestRelayUrls?: boolean |
|
/** |
|
* When true, fold `r.urls` into the author-outbox tier only (no extra first layer). Use for GIF / explicit spell relays |
|
* that should rank with author outboxes, not ahead of user inboxes. Default false: prepend `r.urls` before user tiers. |
|
*/ |
|
mergeSubrequestRelaysIntoAuthorTier?: boolean |
|
} |
|
|
|
/** |
|
* REQ order: user inboxes + locals → author outboxes → favorites → {@link FAST_READ_RELAY_URLS}. |
|
*/ |
|
export function getRelayUrlsWithFavoritesFastReadAndInbox( |
|
favoriteRelays: string[], |
|
blockedRelays: string[], |
|
userInboxReadRelays: string[], |
|
options?: ReadRelayPriorityOptions |
|
): string[] { |
|
const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) |
|
return buildPrioritizedReadRelayUrls({ |
|
userReadRelays: userInboxReadRelays, |
|
userWriteRelays: options?.userWriteRelays ?? [], |
|
authorWriteRelays: options?.authorWriteRelays ?? [], |
|
favoriteRelays: favorites, |
|
blockedRelays, |
|
maxRelays: options?.maxRelays, |
|
applySocialKindBlockedFilter: options?.applySocialKindBlockedFilter |
|
}) |
|
} |
|
|
|
/** |
|
* Profile page pins + feed: viewed author's NIP-65 read + write (REQ tier 1), then logged-in user's favorites, |
|
* then fast-read defaults from constants, deduped and blocked-stripped, capped at this count. |
|
*/ |
|
const PROFILE_PAGE_FEED_MAX_RELAYS = 6 |
|
|
|
export const PROFILE_PAGE_PINS_RESOLVE_LIMIT = 10 |
|
|
|
export function buildProfilePageReadRelayUrls( |
|
favoriteRelays: string[], |
|
blockedRelays: string[], |
|
authorRelayList: { read: string[]; write: string[]; httpRead?: string[]; httpWrite?: string[] }, |
|
kindsIncludeSocialBlockedKind: boolean |
|
): string[] { |
|
return getRelayUrlsWithFavoritesFastReadAndInbox( |
|
favoriteRelays, |
|
blockedRelays, |
|
[...(authorRelayList.httpRead ?? []), ...(authorRelayList.read ?? [])], |
|
{ |
|
userWriteRelays: [...(authorRelayList.httpWrite ?? []), ...(authorRelayList.write ?? [])], |
|
authorWriteRelays: [], |
|
maxRelays: PROFILE_PAGE_FEED_MAX_RELAYS, |
|
applySocialKindBlockedFilter: kindsIncludeSocialBlockedKind |
|
} |
|
) |
|
} |
|
|
|
/** |
|
* Per subrequest: shared inbox → author/favorites → fast read stack, normalized, user-blocked and (when applicable) |
|
* social-kind-blocked stripped, deduped, capped. Subrequest `urls` are prepended first by default (following shards); |
|
* set {@link ReadRelayPriorityOptions.mergeSubrequestRelaysIntoAuthorTier} to fold them into the author tier only |
|
* (e.g. curated GIF / spell relay lists). |
|
*/ |
|
export function augmentSubRequestsWithFavoritesFastReadAndInbox( |
|
requests: TFeedSubRequest[], |
|
favoriteRelays: string[], |
|
blockedRelays: string[], |
|
userInboxReadRelays: string[], |
|
options?: ReadRelayPriorityOptions |
|
): TFeedSubRequest[] { |
|
const max = options?.maxRelays ?? MAX_REQ_RELAY_URLS |
|
const userReadSocialExempt = new Set<string>() |
|
for (const u of userInboxReadRelays) { |
|
const n = normalizeAnyRelayUrl(u) || u.trim() |
|
if (n) userReadSocialExempt.add(n) |
|
} |
|
return requests.map((r) => { |
|
const useSubUrls = options?.mergeSubrequestRelayUrls !== false |
|
const foldIntoAuthor = options?.mergeSubrequestRelaysIntoAuthorTier === true |
|
const applySocial = |
|
options?.applySocialKindBlockedFilter !== undefined |
|
? options.applySocialKindBlockedFilter |
|
: relayFilterIncludesSocialKindBlockedKind(r.filter) |
|
|
|
const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) |
|
|
|
if (!useSubUrls) { |
|
return { |
|
...r, |
|
urls: buildPrioritizedReadRelayUrls({ |
|
userReadRelays: userInboxReadRelays, |
|
userWriteRelays: options?.userWriteRelays ?? [], |
|
authorWriteRelays: options?.authorWriteRelays ?? [], |
|
favoriteRelays: favorites, |
|
blockedRelays, |
|
maxRelays: max, |
|
applySocialKindBlockedFilter: applySocial |
|
}) |
|
} |
|
} |
|
|
|
const authorOnly = dedupeNormalizeRelayUrlsOrdered(options?.authorWriteRelays ?? []) |
|
const authorTier = foldIntoAuthor |
|
? dedupeNormalizeRelayUrlsOrdered([...authorOnly, ...r.urls]) |
|
: authorOnly |
|
|
|
const coreLayers = buildReadRelayPriorityLayers({ |
|
userReadRelays: userInboxReadRelays, |
|
userWriteRelays: options?.userWriteRelays ?? [], |
|
authorWriteRelays: authorTier, |
|
favoriteRelays: favorites |
|
}) |
|
|
|
const layers = foldIntoAuthor ? coreLayers : [relayUrlsLocalsFirst(r.urls), ...coreLayers] |
|
|
|
return { |
|
...r, |
|
urls: mergeRelayPriorityLayers(layers, blockedRelays, max, { |
|
applySocialKindBlockedFilter: applySocial, |
|
exemptNormUrlsFromSocialKindBlock: userReadSocialExempt |
|
}) |
|
} |
|
}) |
|
}
|
|
|