mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Trending-pattern clone ranked by sats from kind-9735 receipts: - js/zapTotals.js: per-pointer receipt aggregation. The feed's metric getter returns the cached total and schedules a batched fetch (#a/#e chunks via the L1 SubscriptionManager) for unknown/stale pointers; amounts parse via nip57 bolt11 with the zap request's amount tag as fallback; receipts are deduped by id so re-fetches never double-count; zero-receipt answers are cached (2-min TTL). - sorters.js: shared count-ranked sorter (metric desc, recency tie-break, author spread, trusted-muted sink); Trending delegates unchanged, Most Zapped ranks on runtime.getZapTotal. - Full wiring: feed definition, view + info popover, change-signal re-rank (order settles as receipts stream in), sidebar link, viewManager registry, feedCoordinator case/loader, FEED_TYPES.MOST_ZAPPED, and a FEATURE_MOST_ZAPPED_FEED flag (default on; off hides the tab). Co-Authored-By: Claude Fable 5 <[email protected]>
246 lines
8.0 KiB
JavaScript
246 lines
8.0 KiB
JavaScript
// js/viewManager.js
|
|
import { initChannelProfileView } from "./channelProfile.js";
|
|
import { initForYouView } from "./forYouView.js";
|
|
import { initKidsView } from "./kidsView.js";
|
|
import { initExploreView } from "./exploreView.js";
|
|
import { initTrendingView } from "./trendingView.js";
|
|
import { initMostZappedView } from "./mostZappedView.js";
|
|
import { subscriptions } from "./subscriptions.js";
|
|
import { getApplication } from "./applicationContext.js";
|
|
import { applyDesignSystemAttributes } from "./designSystem.js";
|
|
import { devLogger, userLogger } from "./utils/logger.js";
|
|
import { attachFeedInfoPopover } from "./ui/components/FeedInfoPopover.js";
|
|
import { initDocsView } from "./docsView.js";
|
|
import { FEED_TYPES } from "./constants.js";
|
|
|
|
const TRACKING_SCRIPT_PATTERN = /(?:^|\/)tracking\.js(?:$|\?)/;
|
|
|
|
/**
|
|
* Load a partial view by URL into the #viewContainer.
|
|
*/
|
|
export async function loadView(viewUrl) {
|
|
try {
|
|
const app = getApplication();
|
|
if (app && typeof app.prepareForViewLoad === "function") {
|
|
app.prepareForViewLoad();
|
|
}
|
|
|
|
const res = await fetch(viewUrl, { cache: "no-store" });
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to load view: ${res.status}`);
|
|
}
|
|
const text = await res.text();
|
|
|
|
// Use a DOMParser to extract the body contents
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(text, "text/html");
|
|
const container = document.getElementById("viewContainer");
|
|
|
|
container.innerHTML = doc.body.innerHTML;
|
|
applyDesignSystemAttributes(container);
|
|
|
|
// Copy and execute any inline scripts
|
|
const scriptTags = doc.querySelectorAll("script");
|
|
scriptTags.forEach((oldScript) => {
|
|
const src = oldScript.getAttribute("src") || "";
|
|
if (src && TRACKING_SCRIPT_PATTERN.test(src)) {
|
|
return;
|
|
}
|
|
const newScript = document.createElement("script");
|
|
Array.from(oldScript.attributes).forEach((attr) => {
|
|
newScript.setAttribute(attr.name, attr.value);
|
|
});
|
|
newScript.textContent = oldScript.textContent;
|
|
container.appendChild(newScript);
|
|
});
|
|
} catch (err) {
|
|
userLogger.error("View loading error:", err);
|
|
const fallbackMarkup = `
|
|
<div class="bv-stack">
|
|
<article class="card p-md" data-state="critical">
|
|
<p class="text-sm text-critical-strong text-center">
|
|
Failed to load content.
|
|
</p>
|
|
</article>
|
|
</div>
|
|
`;
|
|
const fallbackContainer = document.getElementById("viewContainer");
|
|
if (fallbackContainer) {
|
|
fallbackContainer.innerHTML = fallbackMarkup;
|
|
applyDesignSystemAttributes(fallbackContainer);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registry of view-specific initialization functions.
|
|
*/
|
|
export const viewInitRegistry = {
|
|
[FEED_TYPES.RECENT]: () => {
|
|
const app = getApplication();
|
|
if (app && typeof app.loadVideos === "function") {
|
|
if (typeof app.mountVideoListView === "function") {
|
|
app.mountVideoListView();
|
|
}
|
|
app.loadVideos();
|
|
}
|
|
// Force profile updates after the new view is in place.
|
|
const refreshApp = getApplication();
|
|
if (refreshApp && typeof refreshApp.forceRefreshAllProfiles === "function") {
|
|
refreshApp.forceRefreshAllProfiles();
|
|
}
|
|
|
|
const infoTrigger = document.getElementById("recentInfoTrigger");
|
|
if (infoTrigger) {
|
|
attachFeedInfoPopover(
|
|
infoTrigger,
|
|
"Global chronological feed from all relays."
|
|
);
|
|
}
|
|
},
|
|
[FEED_TYPES.FOR_YOU]: () => {
|
|
initForYouView();
|
|
const app = getApplication();
|
|
if (app && typeof app.loadForYouVideos === "function") {
|
|
if (typeof app.mountVideoListView === "function") {
|
|
app.mountVideoListView();
|
|
}
|
|
app.loadForYouVideos();
|
|
}
|
|
// Force profile updates after the new view is in place.
|
|
const refreshApp = getApplication();
|
|
if (refreshApp && typeof refreshApp.forceRefreshAllProfiles === "function") {
|
|
refreshApp.forceRefreshAllProfiles();
|
|
}
|
|
},
|
|
[FEED_TYPES.KIDS]: () => {
|
|
initKidsView();
|
|
const app = getApplication();
|
|
if (app && typeof app.loadKidsVideos === "function") {
|
|
if (typeof app.mountVideoListView === "function") {
|
|
app.mountVideoListView();
|
|
}
|
|
app.loadKidsVideos();
|
|
}
|
|
const refreshApp = getApplication();
|
|
if (refreshApp && typeof refreshApp.forceRefreshAllProfiles === "function") {
|
|
refreshApp.forceRefreshAllProfiles();
|
|
}
|
|
},
|
|
[FEED_TYPES.EXPLORE]: () => {
|
|
initExploreView();
|
|
const app = getApplication();
|
|
if (app && typeof app.loadExploreVideos === "function") {
|
|
if (typeof app.mountVideoListView === "function") {
|
|
app.mountVideoListView({ includeTags: false });
|
|
}
|
|
app.loadExploreVideos();
|
|
}
|
|
const refreshApp = getApplication();
|
|
if (refreshApp && typeof refreshApp.forceRefreshAllProfiles === "function") {
|
|
refreshApp.forceRefreshAllProfiles();
|
|
}
|
|
},
|
|
[FEED_TYPES.TRENDING]: () => {
|
|
const app = getApplication();
|
|
initTrendingView({
|
|
getApp: getApplication,
|
|
isActive: () =>
|
|
typeof window !== "undefined" &&
|
|
typeof window.location?.hash === "string" &&
|
|
window.location.hash.startsWith(`#view=${FEED_TYPES.TRENDING}`),
|
|
});
|
|
if (app && typeof app.loadTrendingVideos === "function") {
|
|
if (typeof app.mountVideoListView === "function") {
|
|
app.mountVideoListView({ includeTags: false });
|
|
}
|
|
app.loadTrendingVideos();
|
|
}
|
|
const refreshApp = getApplication();
|
|
if (refreshApp && typeof refreshApp.forceRefreshAllProfiles === "function") {
|
|
refreshApp.forceRefreshAllProfiles();
|
|
}
|
|
},
|
|
[FEED_TYPES.MOST_ZAPPED]: () => {
|
|
const app = getApplication();
|
|
initMostZappedView({
|
|
getApp: getApplication,
|
|
isActive: () =>
|
|
typeof window !== "undefined" &&
|
|
typeof window.location?.hash === "string" &&
|
|
window.location.hash.startsWith(`#view=${FEED_TYPES.MOST_ZAPPED}`),
|
|
});
|
|
if (app && typeof app.loadMostZappedVideos === "function") {
|
|
if (typeof app.mountVideoListView === "function") {
|
|
app.mountVideoListView({ includeTags: false });
|
|
}
|
|
app.loadMostZappedVideos();
|
|
}
|
|
const refreshApp = getApplication();
|
|
if (refreshApp && typeof refreshApp.forceRefreshAllProfiles === "function") {
|
|
refreshApp.forceRefreshAllProfiles();
|
|
}
|
|
},
|
|
[FEED_TYPES.DOCS]: async () => {
|
|
await initDocsView();
|
|
},
|
|
[FEED_TYPES.HISTORY]: async () => {
|
|
try {
|
|
const module = await import("./historyView.js");
|
|
if (typeof module.initHistoryView === "function") {
|
|
await module.initHistoryView();
|
|
}
|
|
} catch (error) {
|
|
userLogger.error("Failed to initialize history view:", error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Subscriptions view:
|
|
* - If user is logged in, calls subscriptions.showSubscriptionVideos
|
|
* which loads subs if needed and renders the video grid in #subscriptionsVideoList
|
|
*/
|
|
[FEED_TYPES.SUBSCRIPTIONS]: async () => {
|
|
devLogger.log("Subscriptions view loaded.");
|
|
|
|
const infoTrigger = document.getElementById("subscriptionsInfoTrigger");
|
|
if (infoTrigger) {
|
|
attachFeedInfoPopover(
|
|
infoTrigger,
|
|
"Latest videos from channels you follow."
|
|
);
|
|
}
|
|
|
|
const app = getApplication();
|
|
if (!app?.pubkey) {
|
|
const container = document.getElementById("subscriptionsVideoList");
|
|
if (container) {
|
|
container.innerHTML =
|
|
"<p class='text-muted'>Please log in to see your subscriptions.</p>";
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If user is logged in, let the SubscriptionsManager do everything:
|
|
await subscriptions.showSubscriptionVideos(
|
|
app.pubkey,
|
|
"subscriptionsVideoList"
|
|
);
|
|
},
|
|
|
|
[FEED_TYPES.CHANNEL]: () => {
|
|
// Call the initialization function from channelProfile.js
|
|
initChannelProfileView();
|
|
},
|
|
[FEED_TYPES.SEARCH]: async () => {
|
|
try {
|
|
const module = await import("./searchView.js");
|
|
if (typeof module.initSearchView === "function") {
|
|
await module.initSearchView();
|
|
}
|
|
} catch (error) {
|
|
userLogger.error("Failed to initialize search view:", error);
|
|
}
|
|
},
|
|
};
|