// js/searchView.js import { nostrClient } from "./nostrClientFacade.js"; import { getApplication } from "./applicationContext.js"; import { renderChannelVideosFromList } from "./channelProfile.js"; import { escapeHTML } from "./utils/domUtils.js"; import { formatShortNpub } from "./utils/formatters.js"; import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js"; import { devLogger } from "./utils/logger.js"; import { ALLOW_NSFW_CONTENT } from "./config.js"; import { attachFeedInfoPopover } from "./ui/components/FeedInfoPopover.js"; import { setHashView } from "./hashView.js"; import { sanitizeRelayList } from "./nostr/nip46Client.js"; import { subscribeToVideoViewCount, unsubscribeFromVideoViewCount } from "./viewCounter.js"; import { buildSearchHashFromState, getSearchFilterState, resetSearchFilters, setSearchFilterState, syncSearchFilterStateFromHash, } from "./search/searchFilterState.js"; import { DEFAULT_FILTERS, SORT_OPTIONS } from "./search/searchFilters.js"; function getApp() { return getApplication(); } const FALLBACK_AVATAR = "assets/svg/default-profile.svg"; function renderProfileCards(profiles, container) { if (!container) return; container.innerHTML = ""; // Reset the "Show More" button and container limit state const showMoreContainer = document.getElementById("searchChannelShowMore"); if (showMoreContainer) { showMoreContainer.classList.add("hidden"); } container.classList.remove("channel-grid-limit"); if (!profiles || profiles.length === 0) { container.innerHTML = `
No matching channels found.
`; return; } // If we have more than 5 results (the smallest limit), we might need to show the button // depending on screen size, but for simplicity we just enable it if > 5. if (profiles.length > 5) { container.classList.add("channel-grid-limit"); if (showMoreContainer) { showMoreContainer.classList.remove("hidden"); const btn = showMoreContainer.querySelector("button"); if (btn) { btn.onclick = () => { container.classList.remove("channel-grid-limit"); showMoreContainer.classList.add("hidden"); }; } } } const fragment = document.createDocumentFragment(); for (const profile of profiles) { const pubkey = profile.pubkey; const metadata = profile.metadata || {}; const name = metadata.name || metadata.display_name || "Unknown User"; const picture = sanitizeProfileMediaUrl(metadata.picture) || FALLBACK_AVATAR; const about = metadata.about || ""; let npub = ""; try { if (window.NostrTools && window.NostrTools.nip19) { npub = window.NostrTools.nip19.npubEncode(pubkey); } } catch (e) { // ignore } const shortNpub = formatShortNpub(npub) || ""; const card = document.createElement("a"); card.href = `#view=channel-profile&npub=${npub || pubkey}`; card.className = "flex items-center gap-3 p-3 rounded-lg bg-surface hover:bg-surface-elevated transition-colors border border-border group"; card.innerHTML = `${escapeHTML(shortNpub)}
Searching channels...
`; } if (videoList) { videoList.innerHTML = `Searching videos...
`; } if (!query) { if (channelList) channelList.innerHTML = `Please enter a search term.
`; if (videoList) videoList.innerHTML = `Please enter a search term.
`; return; } const searchToken = ++currentSearchToken; // 1. Search Profiles performProfileSearch(query, searchToken, parsedQuery.filters).then(profiles => { if (searchToken !== currentSearchToken) return; renderProfileCards(profiles, channelList); }).catch(err => { devLogger.warn("[Search] Profile search failed", err); if (channelList && searchToken === currentSearchToken) { channelList.innerHTML = `Failed to load channels.
`; } }); // 2. Search Videos performVideoSearch(query, searchToken, parsedQuery.filters).then(videos => { if (searchToken !== currentSearchToken) return; const app = getApp(); renderSearchVideos(videos, videoList, app); }).catch(err => { devLogger.warn("[Search] Video search failed", err); if (videoList && searchToken === currentSearchToken) { videoList.innerHTML = `Failed to load videos.
`; } }); } const formatDateLabel = (timestampSeconds) => { if (!Number.isFinite(timestampSeconds)) return ""; const formatter = new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "2-digit", day: "2-digit", }); return formatter.format(new Date(timestampSeconds * 1000)); }; const formatDurationLabel = (seconds) => { if (!Number.isFinite(seconds)) return ""; if (seconds % 3600 === 0) { return `${seconds / 3600}h`; } if (seconds % 60 === 0) { return `${seconds / 60}m`; } return `${seconds}s`; }; const resolveSortLabel = (sortValue) => { const option = SORT_OPTIONS.find((entry) => entry.value === sortValue); if (!option) return sortValue; const suffix = option.experimental ? " (experimental)" : ""; return `${option.label}${suffix}`; }; const getVideoMetricValue = (video, candidates = []) => { if (!video || typeof video !== "object") { return null; } for (const candidate of candidates) { if (!candidate) continue; const value = video[candidate]; if (Number.isFinite(value)) { return Number(value); } } return null; }; const getVideoZapCount = (video) => { const direct = getVideoMetricValue(video, [ "zapCount", "zapTotal", "zapTotalSats", "zapTotalMsats", ]); if (direct !== null) { return direct; } if (video?.zaps && typeof video.zaps === "object") { const nested = getVideoMetricValue(video.zaps, ["count", "total"]); if (nested !== null) { return nested; } } return null; }; const getVideoReactionCount = (video) => { const direct = getVideoMetricValue(video, [ "reactionCount", "reactionsCount", "commentCount", "commentsCount", ]); if (direct !== null) { return direct; } if (video?.reactions && typeof video.reactions === "object") { const nested = getVideoMetricValue(video.reactions, ["total", "count"]); if (nested !== null) { return nested; } } return null; }; const getViewCountSnapshot = (video, app, relays) => { if (!video) { return { total: null, known: false }; } const pointerInfo = typeof app?.deriveVideoPointerInfo === "function" ? app.deriveVideoPointerInfo(video) : null; if (!pointerInfo?.pointer) { return { total: null, known: false }; } let snapshot = null; let token = null; try { token = subscribeToVideoViewCount( pointerInfo.pointer, (next) => { snapshot = next; }, relays?.length ? { relays } : undefined, ); } catch (error) { devLogger.warn("[Search] Unable to subscribe to view count.", error); return { total: null, known: false }; } finally { if (token) { try { unsubscribeFromVideoViewCount(pointerInfo.pointer, token); } catch (error) { devLogger.warn("[Search] Unable to unsubscribe from view count.", error); } } } const total = Number.isFinite(snapshot?.total) ? Number(snapshot.total) : null; const known = Number.isFinite(snapshot?.lastSyncedAt) && snapshot.lastSyncedAt > 0; return { total, known }; }; function buildNextFilters() { const current = getSearchFilterState(); const filters = current.filters || {}; return { ...filters, dateRange: { ...filters.dateRange }, duration: { ...filters.duration }, authorPubkeys: Array.isArray(filters.authorPubkeys) ? [...filters.authorPubkeys] : [], tags: Array.isArray(filters.tags) ? [...filters.tags] : [], }; } function applyFiltersAndRefresh(nextFilters) { const current = getSearchFilterState(); const nextState = { text: current.text || "", filters: nextFilters, }; setSearchFilterState(nextState); setHashView(buildSearchHashFromState(nextState)); } function renderActiveFilters(filters) { const wrapper = document.getElementById("searchActiveFilters"); const list = document.getElementById("searchActiveFiltersList"); const clearBtn = document.getElementById("searchActiveFiltersClear"); if (!wrapper || !list || !clearBtn) { return; } list.innerHTML = ""; const pills = []; filters.authorPubkeys?.forEach((author) => { pills.push({ label: `Author: ${author}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.authorPubkeys = nextFilters.authorPubkeys.filter( (value) => value !== author, ); applyFiltersAndRefresh(nextFilters); }, }); }); filters.tags?.forEach((tag) => { pills.push({ label: `#${tag}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.tags = nextFilters.tags.filter((value) => value !== tag); applyFiltersAndRefresh(nextFilters); }, }); }); if (Number.isFinite(filters.kind)) { pills.push({ label: `Kind: ${filters.kind}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.kind = null; applyFiltersAndRefresh(nextFilters); }, }); } if (filters.relay) { pills.push({ label: `Relay: ${filters.relay}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.relay = null; applyFiltersAndRefresh(nextFilters); }, }); } if (Number.isFinite(filters.dateRange?.after)) { pills.push({ label: `After: ${formatDateLabel(filters.dateRange.after)}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.dateRange.after = null; applyFiltersAndRefresh(nextFilters); }, }); } if (Number.isFinite(filters.dateRange?.before)) { pills.push({ label: `Before: ${formatDateLabel(filters.dateRange.before)}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.dateRange.before = null; applyFiltersAndRefresh(nextFilters); }, }); } if (Number.isFinite(filters.duration?.minSeconds)) { pills.push({ label: `Min duration: ${formatDurationLabel(filters.duration.minSeconds)}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.duration.minSeconds = null; applyFiltersAndRefresh(nextFilters); }, }); } if (Number.isFinite(filters.duration?.maxSeconds)) { pills.push({ label: `Max duration: ${formatDurationLabel(filters.duration.maxSeconds)}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.duration.maxSeconds = null; applyFiltersAndRefresh(nextFilters); }, }); } if (filters.hasMagnet === true) { pills.push({ label: "Has magnet", onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.hasMagnet = null; applyFiltersAndRefresh(nextFilters); }, }); } if (filters.hasUrl === true) { pills.push({ label: "Has URL", onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.hasUrl = null; applyFiltersAndRefresh(nextFilters); }, }); } if (filters.hasTranscript === true) { pills.push({ label: "Has transcript", onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.hasTranscript = null; applyFiltersAndRefresh(nextFilters); }, }); } if (filters.nsfw && filters.nsfw !== "any") { pills.push({ label: `NSFW: ${filters.nsfw}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.nsfw = "any"; applyFiltersAndRefresh(nextFilters); }, }); } if (filters.sort && filters.sort !== DEFAULT_FILTERS.sort) { pills.push({ label: `Sort: ${resolveSortLabel(filters.sort)}`, onRemove: () => { const nextFilters = buildNextFilters(); nextFilters.sort = DEFAULT_FILTERS.sort; applyFiltersAndRefresh(nextFilters); }, }); } if (!pills.length) { wrapper.classList.add("hidden"); return; } const fragment = document.createDocumentFragment(); pills.forEach((pill) => { const button = document.createElement("button"); button.type = "button"; button.className = "search-filter-chip inline-flex items-center gap-2 text-xs"; const label = document.createElement("span"); label.textContent = pill.label; const remove = document.createElement("span"); remove.setAttribute("aria-hidden", "true"); remove.textContent = "✕"; button.append(label, remove); button.addEventListener("click", pill.onRemove); fragment.appendChild(button); }); list.appendChild(fragment); wrapper.classList.remove("hidden"); clearBtn.onclick = () => { resetSearchFilters(); const nextState = getSearchFilterState(); setHashView(buildSearchHashFromState(nextState)); }; } async function renderSearchVideos(videos, container, app) { if (!container) return; container.innerHTML = ""; if (!videos || videos.length === 0) { container.innerHTML = `No matching videos found.
`; return; } // Dynamic import VideoCard to avoid circular dependencies if any const { VideoCard } = await import("./ui/components/VideoCard.js"); const { escapeHTML } = await import("./utils/domUtils.js"); // Ensure availability const fragment = document.createDocumentFragment(); // We need some helpers from app or utils const loadedThumbnails = app?.loadedThumbnails instanceof Map ? app.loadedThumbnails : new Map(); let index = 0; for (const video of videos) { if (!video) continue; // Basic decoration if needed (e.g. resolve author info if not present) // For search results, we might not have full author profiles loaded. // We will do best effort. const shareUrl = app?.buildShareUrlFromEventId ? app.buildShareUrlFromEventId(video.id) : "#"; const timeAgo = new Date(video.created_at * 1000).toLocaleString(); // Construct identity object const identity = { name: video.authorName || "", picture: video.authorPicture || "", pubkey: video.pubkey, npub: video.authorNpub || "", shortNpub: video.shortNpub || "" }; // If we have access to the profile cache, we could enrich this. // For now, let's rely on what's in the video object or defaults. const videoCard = new VideoCard({ document, video, index: index++, shareUrl, timeAgo, cardState: "", // default identity, capabilities: { canEdit: false, canDelete: false }, nsfwContext: { isNsfw: video.isNsfw, allowNsfw: true, // or import config viewerIsOwner: false }, helpers: { escapeHtml: escapeHTML, isMagnetSupported: (magnet) => magnet && magnet.startsWith("magnet:"), toLocaleString: (val) => val.toLocaleString() }, assets: { fallbackThumbnailSrc: "assets/jpg/video-thumbnail-fallback.jpg", unsupportedBtihMessage: "Unsupported magnet link" }, state: { loadedThumbnails }, // Callbacks can be wired if we want full interactivity (play, etc) ensureGlobalMoreMenuHandlers: () => {}, onRequestCloseAllMenus: () => {} }); // Wire up Play videoCard.onPlay = ({ event: domEvent }) => { // We can reuse the global playback handler if available if (app?.playVideoByEventId) { app.playVideoByEventId(video.id, { url: video.url, magnet: video.magnet, title: video.title }); } else if (app?.playVideoWithFallback) { app.playVideoWithFallback({ url: video.url, magnet: video.magnet }); } }; videoCard.onAuthorNavigate = ({ pubkey }) => { if (app?.goToProfile) { app.goToProfile(pubkey || video.pubkey); } }; const el = videoCard.getRoot(); if (el) fragment.appendChild(el); } container.appendChild(fragment); } async function performProfileSearch(query, token, filters = {}) { if (!query) return []; const normalizedQuery = query.toLowerCase(); const uniqueProfiles = new Map(); const authorFilterSet = new Set( Array.isArray(filters?.authorPubkeys) ? filters.authorPubkeys.map((author) => String(author || "").toLowerCase()) : [] ); const afterDate = Number.isFinite(filters?.dateRange?.after) ? filters.dateRange.after : null; const beforeDate = Number.isFinite(filters?.dateRange?.before) ? filters.dateRange.before : null; // 1. Local Search (Scan known authors in cache) if (nostrClient?.activeMap instanceof Map) { const app = getApp(); if (app && typeof app.getProfileCacheEntry === 'function') { const seenPubkeys = new Set(); for (const video of nostrClient.activeMap.values()) { if (!video || !video.pubkey || seenPubkeys.has(video.pubkey)) continue; seenPubkeys.add(video.pubkey); const entry = app.getProfileCacheEntry(video.pubkey); if (entry?.profile) { const name = entry.profile.name || entry.profile.display_name || ""; const about = entry.profile.about || ""; const nip05 = entry.profile.nip05 || ""; const lightning = entry.profile.lightningAddress || ""; if (name.toLowerCase().includes(normalizedQuery) || about.toLowerCase().includes(normalizedQuery) || nip05.toLowerCase().includes(normalizedQuery) || lightning.toLowerCase().includes(normalizedQuery)) { uniqueProfiles.set(video.pubkey, { pubkey: video.pubkey, metadata: entry.profile, created_at: entry.event?.created_at || 0 }); } } else if (video.authorName && video.authorName.toLowerCase().includes(normalizedQuery)) { // Fallback to video metadata if profile cache missing uniqueProfiles.set(video.pubkey, { pubkey: video.pubkey, metadata: { name: video.authorName, picture: video.authorPicture, nip05: video.authorNip05 }, created_at: 0 }); } } } } if (currentSearchToken !== token) return []; // 2. Relay Search (NIP-50) if (nostrClient && nostrClient.pool) { const relaySelection = filters?.relay ? sanitizeRelayList([filters.relay]) : null; const relays = relaySelection?.length ? relaySelection : Array.isArray(nostrClient.relays) ? nostrClient.relays : []; if (relays.length > 0) { const filter = { kinds: [Number.isFinite(filters?.kind) ? filters.kind : 0], search: query, limit: 20 }; const authorFilters = Array.isArray(filters?.authorPubkeys) ? filters.authorPubkeys.filter((author) => typeof author === "string" && author) : []; if (authorFilters.length > 0) { filter.authors = authorFilters; } if (Number.isFinite(afterDate)) { filter.since = afterDate; } if (Number.isFinite(beforeDate)) { filter.until = beforeDate; } const tagFilters = Array.isArray(filters?.tags) ? filters.tags .map((tag) => String(tag || "").trim().toLowerCase()) .filter(Boolean) : []; if (tagFilters.length > 0) { filter["#t"] = Array.from(new Set(tagFilters)); } try { const events = await nostrClient.pool.list(relays, [filter]); if (currentSearchToken === token) { for (const event of events) { try { const metadata = JSON.parse(event.content); // Merge/Overwrite with newer const existing = uniqueProfiles.get(event.pubkey); if (!existing || existing.created_at < event.created_at) { uniqueProfiles.set(event.pubkey, { pubkey: event.pubkey, metadata, created_at: event.created_at }); } } catch (e) { // ignore invalid content } } } } catch (e) { devLogger.warn("Profile relay search error", e); } } } if (currentSearchToken !== token) return []; // Convert map to array and sort by something relevant (maybe creation time or exact match?) return Array.from(uniqueProfiles.values()).filter((profile) => { if (!profile) return false; if (authorFilterSet.size > 0) { const pubkey = typeof profile.pubkey === "string" ? profile.pubkey.toLowerCase() : ""; if (!pubkey || !authorFilterSet.has(pubkey)) return false; } if (Number.isFinite(afterDate) && (!Number.isFinite(profile.created_at) || profile.created_at < afterDate)) { return false; } if (Number.isFinite(beforeDate) && (!Number.isFinite(profile.created_at) || profile.created_at > beforeDate)) { return false; } return true; }); } async function performVideoSearch(query, token, filters = {}) { if (!nostrClient) return []; const isHashtag = query.startsWith("#"); const term = isHashtag ? query.slice(1) : query; const lowerTerm = term.toLowerCase(); const app = getApp(); const nostrService = app?.nostrService; const filterOptions = { blacklistedEventIds: app?.blacklistedEventIds instanceof Set ? new Set(app.blacklistedEventIds) : new Set(), isAuthorBlocked: typeof app?.isAuthorBlocked === "function" ? (pubkey) => app.isAuthorBlocked(pubkey) : () => false }; const applyAccessFilters = (videos) => nostrService?.filterVideos ? nostrService.filterVideos(videos, filterOptions) : videos; const authorSet = new Set( Array.isArray(filters?.authorPubkeys) ? filters.authorPubkeys.map((author) => author.toLowerCase()) : [] ); const tagSet = new Set( Array.isArray(filters?.tags) ? filters.tags.map((tag) => tag.toLowerCase()) : [] ); const afterDate = Number.isFinite(filters?.dateRange?.after) ? filters.dateRange.after : null; const beforeDate = Number.isFinite(filters?.dateRange?.before) ? filters.dateRange.before : null; const minDuration = Number.isFinite(filters?.duration?.minSeconds) ? filters.duration.minSeconds : null; const maxDuration = Number.isFinite(filters?.duration?.maxSeconds) ? filters.duration.maxSeconds : null; const hasMagnet = filters?.hasMagnet === true; const hasUrl = filters?.hasUrl === true; const hasTranscript = filters?.hasTranscript === true; const nsfwFilter = typeof filters?.nsfw === "string" ? filters.nsfw : "any"; const matchesRelayFilters = (video) => { if (!video) return false; if (authorSet.size > 0) { const pubkey = typeof video.pubkey === "string" ? video.pubkey.toLowerCase() : ""; if (!pubkey || !authorSet.has(pubkey)) return false; } if (tagSet.size > 0) { const tags = Array.isArray(video.tags) ? video.tags : []; const hasTag = tags.some((tag) => { if (Array.isArray(tag)) { return tag[0] === "t" && tagSet.has(String(tag[1] || "").toLowerCase()); } return tagSet.has(String(tag || "").toLowerCase()); }); if (!hasTag) return false; } if (Number.isFinite(afterDate) && (!Number.isFinite(video.created_at) || video.created_at < afterDate)) { return false; } if (Number.isFinite(beforeDate) && (!Number.isFinite(video.created_at) || video.created_at > beforeDate)) { return false; } return true; }; const matchesUnsupportedFilters = (video) => { if (!video) return false; if (hasMagnet && !video.magnet) return false; if (hasUrl && !video.url) return false; if (hasTranscript) { const textTracks = Array.isArray(video.nip71?.textTracks) ? video.nip71.textTracks : []; if (!textTracks.length) return false; } if (Number.isFinite(minDuration) || Number.isFinite(maxDuration)) { const duration = Number(video.nip71?.duration); if (!Number.isFinite(duration)) return false; if (Number.isFinite(minDuration) && duration < minDuration) return false; if (Number.isFinite(maxDuration) && duration > maxDuration) return false; } if (ALLOW_NSFW_CONTENT !== true && video.isNsfw === true) { return false; } if (ALLOW_NSFW_CONTENT === true) { if ((nsfwFilter === "true" || nsfwFilter === "only") && video.isNsfw !== true) { return false; } if (nsfwFilter === "false" && video.isNsfw === true) { return false; } } return true; }; // 1. Local Search (Cache) const localMatches = []; if (nostrClient.activeMap instanceof Map) { for (const video of nostrClient.activeMap.values()) { if (!video || video.deleted) continue; let matches = false; if (isHashtag) { // Check tags for 't' if (Array.isArray(video.tags)) { matches = video.tags.some(t => Array.isArray(t) && t[0] === "t" && t[1].toLowerCase() === lowerTerm); } } else { // Check title/description const title = video.title ? video.title.toLowerCase() : ""; const desc = video.description ? video.description.toLowerCase() : ""; if (title.includes(lowerTerm) || desc.includes(lowerTerm)) { matches = true; } } if (matches) { localMatches.push(video); } } } if (currentSearchToken !== token) return []; const filteredLocalMatches = applyAccessFilters(localMatches).filter( (video) => matchesRelayFilters(video) && matchesUnsupportedFilters(video) ); // 2. Relay Search let relayVideos = []; const relaySelection = filters?.relay ? sanitizeRelayList([filters.relay]) : null; const relays = relaySelection?.length ? relaySelection : Array.isArray(nostrClient.relays) ? nostrClient.relays : []; if (relays.length > 0 && nostrClient.pool) { const filter = { kinds: [Number.isFinite(filters?.kind) ? filters.kind : 30078], limit: 50 }; const authorFilters = Array.isArray(filters?.authorPubkeys) ? filters.authorPubkeys.filter((author) => typeof author === "string" && author) : []; if (authorFilters.length > 0) { filter.authors = authorFilters; } if (Number.isFinite(afterDate)) { filter.since = afterDate; } if (Number.isFinite(beforeDate)) { filter.until = beforeDate; } const tagFilters = new Set( Array.isArray(filters?.tags) ? filters.tags.map((tag) => String(tag || "").trim().toLowerCase()).filter(Boolean) : [] ); if (isHashtag && term) { tagFilters.add(term.toLowerCase()); } if (tagFilters.size > 0) { filter["#t"] = Array.from(tagFilters); } filter.search = term; try { const events = await nostrClient.pool.list(relays, [filter]); if (currentSearchToken === token) { const { convertEventToVideo } = await import("./nostr/index.js"); relayVideos = events.map(evt => { const vid = convertEventToVideo(evt); if (vid.invalid) return null; return vid; }).filter(Boolean); } } catch (e) { devLogger.warn("Video search error", e); // Continue with local matches if relay search fails } } if (currentSearchToken !== token) return []; const filteredRelayVideos = applyAccessFilters(relayVideos).filter( (video) => matchesRelayFilters(video) && matchesUnsupportedFilters(video) ); // Deduplicate by ID const unique = new Map(); // Add local matches first for (const v of filteredLocalMatches) { if (!unique.has(v.id)) unique.set(v.id, v); } // Add relay matches (overwriting local if needed, or ignoring duplicates) for (const v of filteredRelayVideos) { if (!unique.has(v.id)) unique.set(v.id, v); } const entries = Array.from(unique.values()).map((video, index) => ({ video, index, createdAt: Number.isFinite(video?.created_at) ? Number(video.created_at) : 0 })); const sortByNewest = () => entries .slice() .sort((a, b) => (b.createdAt - a.createdAt) || (a.index - b.index)); const sortByOldest = () => entries .slice() .sort((a, b) => (a.createdAt - b.createdAt) || (a.index - b.index)); const sortByMetric = (metricKey, metricResolver) => { let hasUnknown = false; const enriched = entries.map((entry) => { const metricValue = metricResolver(entry.video); if (!Number.isFinite(metricValue)) { hasUnknown = true; } return { ...entry, [metricKey]: Number.isFinite(metricValue) ? Number(metricValue) : 0 }; }); if (hasUnknown) { return sortByNewest(); } return enriched .sort((a, b) => (b[metricKey] - a[metricKey]) || (b.createdAt - a.createdAt) || (a.index - b.index) ); }; const sortByViews = () => { let hasUnknown = false; const viewCountRelays = relaySelection?.length ? relaySelection : null; const enriched = entries.map((entry) => { const snapshot = getViewCountSnapshot(entry.video, app, viewCountRelays); if (!snapshot.known || !Number.isFinite(snapshot.total)) { hasUnknown = true; } return { ...entry, viewCount: Number.isFinite(snapshot.total) ? Number(snapshot.total) : 0 }; }); if (hasUnknown) { return sortByNewest(); } return enriched .sort((a, b) => (b.viewCount - a.viewCount) || (b.createdAt - a.createdAt) || (a.index - b.index) ); }; const sortMode = typeof filters?.sort === "string" ? filters.sort : DEFAULT_FILTERS.sort; let sortedEntries = sortByNewest(); if (sortMode === "oldest") { sortedEntries = sortByOldest(); } else if (sortMode === "views") { sortedEntries = sortByViews(); } else if (sortMode === "zaps") { sortedEntries = sortByMetric("zapCount", getVideoZapCount); } else if (sortMode === "reactions") { sortedEntries = sortByMetric("reactionCount", getVideoReactionCount); } else if (sortMode === "newest" || sortMode === "relevance") { sortedEntries = sortByNewest(); } return sortedEntries.map((entry) => entry.video); }