From ab607dc7b8a95e82d19a7281ce0781f2d6dc4fd5 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:36:41 -0400 Subject: [PATCH] Add watch history view --- assets/svg/history-icon.svg | 22 ++++ components/sidebar.html | 11 ++ js/historyView.js | 220 ++++++++++++++++++++++++++++++++++++ js/viewManager.js | 10 ++ views/history.html | 19 ++++ 5 files changed, 282 insertions(+) create mode 100644 assets/svg/history-icon.svg create mode 100644 js/historyView.js create mode 100644 views/history.html diff --git a/assets/svg/history-icon.svg b/assets/svg/history-icon.svg new file mode 100644 index 00000000..c224ce68 --- /dev/null +++ b/assets/svg/history-icon.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/components/sidebar.html b/components/sidebar.html index beaab150..dd133e69 100644 --- a/components/sidebar.html +++ b/components/sidebar.html @@ -34,6 +34,17 @@ /> Explore + + + History + { + 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; diff --git a/js/viewManager.js b/js/viewManager.js index 741cd0a2..30671ed8 100644 --- a/js/viewManager.js +++ b/js/viewManager.js @@ -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: diff --git a/views/history.html b/views/history.html new file mode 100644 index 00000000..fa69bf07 --- /dev/null +++ b/views/history.html @@ -0,0 +1,19 @@ + + + + Watch History + + + + + + + Your watch history is empty. Watch some videos to populate this list. + + + +
+ Your watch history is empty. Watch some videos to populate this list. +