Add watch history view

This commit is contained in:
thePR0M3TH3AN
2025-09-30 10:36:41 -04:00
parent 7260dfe1e0
commit ab607dc7b8
5 changed files with 282 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
width="100%"
height="100%"
viewBox="0 0 24 24"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
>
<g transform="matrix(1,0,0,1,12,12)">
<g transform="matrix(1,0,0,1,-12,-12)">
<path
d="M12,2C6.486,2 2,6.486 2,12C2,17.514 6.486,22 12,22C17.514,22 22,17.514 22,12C22,6.486 17.514,2 12,2ZM12,4C16.411,4 20,7.589 20,12C20,16.411 16.411,20 12,20C7.589,20 4,16.411 4,12C4,7.589 7.589,4 12,4ZM11,7C10.448,7 10,7.448 10,8L10,13C10,13.266 10.105,13.52 10.293,13.707L13.293,16.707C13.683,17.098 14.317,17.098 14.707,16.707C15.098,16.317 15.098,15.683 14.707,15.293L12,12.586L12,8C12,7.448 11.552,7 11,7Z"
style="fill:rgb(92,111,138);fill-rule:nonzero;"
/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -34,6 +34,17 @@
/>
<span class="sidebar-text">Explore</span>
</a>
<a
href="#view=history"
class="flex items-center py-2 px-4 hover:bg-gray-700 rounded font-semibold"
>
<img
src="assets/svg/history-icon.svg"
alt="History"
class="w-6 h-6 mr-3 flex-shrink-0"
/>
<span class="sidebar-text">History</span>
</a>
<a
id="subscriptionsLink"
href="#view=subscriptions"

220
js/historyView.js Normal file
View File

@@ -0,0 +1,220 @@
// js/historyView.js
import { nostrClient } from "./nostr.js";
import { WATCH_HISTORY_BATCH_RESOLVE } from "./config.js";
import { subscriptions } from "./subscriptions.js";
const DEFAULT_BATCH_SIZE = 20;
const BATCH_SIZE = WATCH_HISTORY_BATCH_RESOLVE ? DEFAULT_BATCH_SIZE : 1;
const EMPTY_COPY =
"Your watch history is empty. Watch some videos to populate this list.";
let isLoading = false;
let hasMore = true;
let observer = null;
let scrollHandler = null;
let resolvedVideos = [];
function getElements() {
return {
view: document.getElementById("watchHistoryView"),
grid: document.getElementById("watchHistoryGrid"),
loadingEl: document.getElementById("watchHistoryLoading"),
emptyEl: document.getElementById("watchHistoryEmpty"),
sentinel: document.getElementById("watchHistorySentinel"),
};
}
function resetUiState() {
const { grid, loadingEl, emptyEl } = getElements();
if (grid) {
grid.innerHTML = "";
grid.classList.add("hidden");
}
if (loadingEl) {
loadingEl.classList.remove("hidden");
}
if (emptyEl) {
emptyEl.textContent = EMPTY_COPY;
emptyEl.classList.add("hidden");
}
}
function cleanupObservers() {
if (observer) {
observer.disconnect();
observer = null;
}
if (scrollHandler) {
window.removeEventListener("scroll", scrollHandler);
scrollHandler = null;
}
}
function setLoadingVisible(visible) {
const { loadingEl } = getElements();
if (!loadingEl) {
return;
}
if (visible) {
loadingEl.classList.remove("hidden");
} else {
loadingEl.classList.add("hidden");
}
}
function showEmptyState(message = EMPTY_COPY) {
const { grid, emptyEl } = getElements();
if (grid) {
grid.innerHTML = "";
grid.classList.add("hidden");
}
if (emptyEl) {
emptyEl.textContent = message;
emptyEl.classList.remove("hidden");
}
setLoadingVisible(false);
hasMore = false;
cleanupObservers();
}
function mergeResolvedVideos(nextVideos) {
if (!Array.isArray(nextVideos) || !nextVideos.length) {
return;
}
const dedupeMap = new Map();
resolvedVideos.forEach((video) => {
if (video && video.id) {
dedupeMap.set(video.id, video);
}
});
nextVideos.forEach((video) => {
if (video && video.id) {
dedupeMap.set(video.id, video);
}
});
resolvedVideos = Array.from(dedupeMap.values());
}
function renderResolvedVideos() {
const { grid, emptyEl } = getElements();
if (!grid) {
return;
}
if (!resolvedVideos.length) {
if (emptyEl) {
emptyEl.textContent = EMPTY_COPY;
emptyEl.classList.remove("hidden");
}
grid.classList.add("hidden");
return;
}
subscriptions.renderSameGridStyle(resolvedVideos, "watchHistoryGrid");
grid.classList.remove("hidden");
if (emptyEl) {
emptyEl.classList.add("hidden");
}
}
async function loadNextBatch({ initial = false } = {}) {
if (isLoading || !hasMore) {
if (initial && !isLoading && resolvedVideos.length === 0) {
setLoadingVisible(false);
}
return;
}
isLoading = true;
try {
const videos = await nostrClient.resolveWatchHistory(BATCH_SIZE);
if (videos.length === 0) {
if (initial && resolvedVideos.length === 0) {
showEmptyState();
} else {
hasMore = false;
cleanupObservers();
}
return;
}
mergeResolvedVideos(videos);
renderResolvedVideos();
setLoadingVisible(false);
} catch (error) {
console.error("[historyView] Failed to resolve watch history:", error);
if (initial && resolvedVideos.length === 0) {
showEmptyState("We couldn't load your watch history. Please try again later.");
}
} finally {
isLoading = false;
}
}
function attachObservers() {
const { sentinel } = getElements();
if (!sentinel || !hasMore) {
return;
}
if ("IntersectionObserver" in window) {
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadNextBatch();
}
});
},
{ rootMargin: "300px 0px" }
);
observer.observe(sentinel);
return;
}
scrollHandler = () => {
const rect = sentinel.getBoundingClientRect();
if (rect.top <= window.innerHeight + 200) {
loadNextBatch();
}
};
window.addEventListener("scroll", scrollHandler, { passive: true });
scrollHandler();
}
export async function initHistoryView() {
const { view } = getElements();
if (!view) {
return;
}
cleanupObservers();
resolvedVideos = [];
hasMore = true;
isLoading = false;
resetUiState();
try {
const actor = window.app?.pubkey || undefined;
await nostrClient.fetchWatchHistory(actor);
} catch (error) {
console.error("[historyView] Failed to fetch watch history list:", error);
showEmptyState("We couldn't load your watch history. Please try again later.");
return;
}
await loadNextBatch({ initial: true });
if (hasMore) {
attachObservers();
}
}
// Expose init on window for debugging/manual triggers if needed.
if (!window.bitvid) {
window.bitvid = {};
}
window.bitvid.initHistoryView = initHistoryView;

View File

@@ -67,6 +67,16 @@ export const viewInitRegistry = {
explore: () => {
console.log("Explore view loaded.");
},
history: async () => {
try {
const module = await import("./historyView.js");
if (typeof module.initHistoryView === "function") {
await module.initHistoryView();
}
} catch (error) {
console.error("Failed to initialize history view:", error);
}
},
/**
* Subscriptions view:

19
views/history.html Normal file
View File

@@ -0,0 +1,19 @@
<!-- views/history.html -->
<section id="watchHistoryView" class="space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-semibold text-white">Watch History</h2>
</div>
<div id="watchHistoryLoading" class="flex justify-center py-12">
<div class="h-10 w-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
<div
id="watchHistoryGrid"
class="hidden"
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 2rem;"
></div>
<p id="watchHistoryEmpty" class="hidden text-gray-400 text-center py-12">
Your watch history is empty. Watch some videos to populate this list.
</p>
<div id="watchHistorySentinel" class="h-1 w-full"></div>
<script type="module" src="js/historyView.js"></script>
</section>