mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 10:52:27 +00:00
Merge pull request #172 from PR0M3TH3AN/codex/add-profile-modal-history-tab-xus26x
Add modal watch history pane and shared renderer
This commit is contained in:
8 files changed
+772
-313
No files matched your search
@@ -88,6 +88,15 @@
|
||||
>
|
||||
Blocked
|
||||
</button>
|
||||
<button
|
||||
id="profileNavHistory"
|
||||
type="button"
|
||||
class="profile-nav-button px-4 py-2 rounded-md text-sm font-medium text-gray-400 hover:text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
|
||||
aria-controls="profilePaneHistory"
|
||||
aria-selected="false"
|
||||
>
|
||||
Watch history
|
||||
</button>
|
||||
<button
|
||||
id="profileNavAdmin"
|
||||
type="button"
|
||||
@@ -272,6 +281,36 @@
|
||||
</div>
|
||||
<ul id="blockedList" class="space-y-3"></ul>
|
||||
</section>
|
||||
<section
|
||||
id="profilePaneHistory"
|
||||
class="profile-pane hidden space-y-4"
|
||||
role="tabpanel"
|
||||
aria-labelledby="profileNavHistory"
|
||||
>
|
||||
<p class="text-sm text-gray-400">
|
||||
Your recently watched videos appear here so you can jump back into favorites.
|
||||
</p>
|
||||
<div class="space-y-4">
|
||||
<div id="profileHistoryLoading" class="flex justify-center py-6">
|
||||
<div class="h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<div
|
||||
id="profileHistoryEmpty"
|
||||
class="hidden rounded-lg border border-dashed border-gray-700 p-6 text-center text-sm text-gray-400"
|
||||
>
|
||||
You haven’t watched any videos yet.
|
||||
</div>
|
||||
<div id="profileHistoryScroll" class="relative max-h-[420px] overflow-y-auto pr-1">
|
||||
<div
|
||||
id="profileHistoryGrid"
|
||||
class="hidden"
|
||||
role="list"
|
||||
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem;"
|
||||
></div>
|
||||
<div id="profileHistorySentinel" class="h-1 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
id="profilePaneAdmin"
|
||||
class="profile-pane hidden space-y-6"
|
||||
|
||||
+26
-2
@@ -200,8 +200,32 @@
|
||||
<!-- Additional Scripts -->
|
||||
<script src="js/libs/nostr.bundle.js"></script>
|
||||
<script type="module">
|
||||
import { nip19, SimplePool } from "https://esm.sh/[email protected]";
|
||||
window.NostrTools = { nip19, SimplePool };
|
||||
import * as nostrTools from "https://esm.sh/[email protected]";
|
||||
|
||||
const existing =
|
||||
typeof window !== "undefined" &&
|
||||
window.NostrTools &&
|
||||
typeof window.NostrTools === "object"
|
||||
? window.NostrTools
|
||||
: {};
|
||||
|
||||
const merged = { ...existing, ...nostrTools };
|
||||
|
||||
if (
|
||||
!merged.generatePrivateKey &&
|
||||
typeof merged.generateSecretKey === "function"
|
||||
) {
|
||||
merged.generatePrivateKey = merged.generateSecretKey.bind(merged);
|
||||
}
|
||||
|
||||
if (
|
||||
!merged.generateSecretKey &&
|
||||
typeof merged.generatePrivateKey === "function"
|
||||
) {
|
||||
merged.generateSecretKey = merged.generatePrivateKey.bind(merged);
|
||||
}
|
||||
|
||||
window.NostrTools = merged;
|
||||
</script>
|
||||
<script type="module" src="js/config.js"></script>
|
||||
<script type="module" src="js/lists.js"></script>
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
ensureBucketCors,
|
||||
} from "./storage/r2-s3.js";
|
||||
import { initQuickR2Upload } from "./r2-quick.js";
|
||||
import { createWatchHistoryRenderer } from "./historyView.js";
|
||||
|
||||
function truncateMiddle(text, maxLength = 72) {
|
||||
if (!text || typeof text !== "string") {
|
||||
@@ -543,12 +544,14 @@ class bitvidApp {
|
||||
account: null,
|
||||
relays: null,
|
||||
blocked: null,
|
||||
history: null,
|
||||
admin: null,
|
||||
};
|
||||
this.profilePaneElements = {
|
||||
account: null,
|
||||
relays: null,
|
||||
blocked: null,
|
||||
history: null,
|
||||
admin: null,
|
||||
};
|
||||
this.profileRelayList = null;
|
||||
@@ -559,6 +562,8 @@ class bitvidApp {
|
||||
this.profileRelayInput = null;
|
||||
this.profileAddRelayBtn = null;
|
||||
this.profileRestoreRelaysBtn = null;
|
||||
this.profileHistoryRenderer = null;
|
||||
this.activeProfilePane = null;
|
||||
this.adminModeratorInput = null;
|
||||
this.adminAddModeratorBtn = null;
|
||||
this.adminModeratorList = null;
|
||||
@@ -3915,6 +3920,8 @@ class bitvidApp {
|
||||
document.getElementById("profileNavRelays") || null;
|
||||
this.profileNavButtons.blocked =
|
||||
document.getElementById("profileNavBlocked") || null;
|
||||
this.profileNavButtons.history =
|
||||
document.getElementById("profileNavHistory") || null;
|
||||
this.profileNavButtons.admin =
|
||||
document.getElementById("profileNavAdmin") || null;
|
||||
this.profilePaneElements.account =
|
||||
@@ -3923,6 +3930,8 @@ class bitvidApp {
|
||||
document.getElementById("profilePaneRelays") || null;
|
||||
this.profilePaneElements.blocked =
|
||||
document.getElementById("profilePaneBlocked") || null;
|
||||
this.profilePaneElements.history =
|
||||
document.getElementById("profilePaneHistory") || null;
|
||||
this.profilePaneElements.admin =
|
||||
document.getElementById("profilePaneAdmin") || null;
|
||||
this.profileRelayList = document.getElementById("relayList") || null;
|
||||
@@ -3937,6 +3946,21 @@ class bitvidApp {
|
||||
this.profileAddRelayBtn = document.getElementById("addRelayBtn") || null;
|
||||
this.profileRestoreRelaysBtn =
|
||||
document.getElementById("restoreRelaysBtn") || null;
|
||||
if (!this.profileHistoryRenderer) {
|
||||
this.profileHistoryRenderer = createWatchHistoryRenderer({
|
||||
viewSelector: "#profilePaneHistory",
|
||||
gridSelector: "#profileHistoryGrid",
|
||||
loadingSelector: "#profileHistoryLoading",
|
||||
emptySelector: "#profileHistoryEmpty",
|
||||
sentinelSelector: "#profileHistorySentinel",
|
||||
scrollContainerSelector: "#profileHistoryScroll",
|
||||
emptyCopy: "You haven’t watched any videos yet.",
|
||||
beforeInitialLoad: async () => {
|
||||
const actor = this.pubkey || undefined;
|
||||
await nostrClient.fetchWatchHistory(actor);
|
||||
},
|
||||
});
|
||||
}
|
||||
this.adminModeratorsSection =
|
||||
document.getElementById("adminModeratorsSection") || null;
|
||||
this.adminModeratorsEmpty =
|
||||
@@ -4138,6 +4162,7 @@ class bitvidApp {
|
||||
|
||||
selectProfilePane(name = "account") {
|
||||
const normalized = typeof name === "string" ? name.toLowerCase() : "account";
|
||||
const previous = this.activeProfilePane;
|
||||
const availableKeys = Object.keys(this.profilePaneElements).filter((key) => {
|
||||
const pane = this.profilePaneElements[key];
|
||||
if (!(pane instanceof HTMLElement)) {
|
||||
@@ -4157,6 +4182,14 @@ class bitvidApp {
|
||||
? normalized
|
||||
: fallbackTarget;
|
||||
|
||||
if (previous === "history" && target !== "history") {
|
||||
try {
|
||||
this.profileHistoryRenderer?.pause();
|
||||
} catch (error) {
|
||||
console.warn("[profileModal] Failed to pause history renderer:", error);
|
||||
}
|
||||
}
|
||||
|
||||
Object.entries(this.profilePaneElements).forEach(([key, pane]) => {
|
||||
if (!(pane instanceof HTMLElement)) {
|
||||
return;
|
||||
@@ -4178,6 +4211,11 @@ class bitvidApp {
|
||||
});
|
||||
|
||||
this.updateProfileModalFocusables();
|
||||
this.activeProfilePane = target;
|
||||
|
||||
if (target === "history") {
|
||||
void this.populateProfileWatchHistory();
|
||||
}
|
||||
}
|
||||
|
||||
updateProfileModalFocusables() {
|
||||
@@ -4220,6 +4258,7 @@ class bitvidApp {
|
||||
console.error("Failed to refresh admin pane while opening profile modal:", error);
|
||||
}
|
||||
|
||||
this.activeProfilePane = null;
|
||||
this.selectProfilePane("account");
|
||||
this.populateProfileRelays();
|
||||
try {
|
||||
@@ -4342,6 +4381,17 @@ class bitvidApp {
|
||||
|
||||
this.closeAllMoreMenus();
|
||||
|
||||
try {
|
||||
this.profileHistoryRenderer?.destroy();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[profileModal] Failed to reset watch history renderer on close:",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
this.activeProfilePane = null;
|
||||
|
||||
if (
|
||||
this.lastFocusedBeforeProfileModal &&
|
||||
typeof this.lastFocusedBeforeProfileModal.focus === "function"
|
||||
@@ -4549,6 +4599,22 @@ class bitvidApp {
|
||||
});
|
||||
}
|
||||
|
||||
async populateProfileWatchHistory() {
|
||||
if (!this.profileHistoryRenderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.profileHistoryRenderer.ensureInitialLoad();
|
||||
this.profileHistoryRenderer.resume();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[profileModal] Failed to populate watch history pane:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async handleAddProfile() {
|
||||
if (!this.profileAddAccountBtn) {
|
||||
return;
|
||||
@@ -5506,6 +5572,12 @@ class bitvidApp {
|
||||
}
|
||||
|
||||
attachVideoListHandler() {
|
||||
const nextTarget = document;
|
||||
|
||||
if (this._videoListElement === nextTarget && this._videoListClickHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._videoListElement && this._videoListClickHandler) {
|
||||
this._videoListElement.removeEventListener(
|
||||
"click",
|
||||
@@ -5515,10 +5587,6 @@ class bitvidApp {
|
||||
this._videoListClickHandler = null;
|
||||
}
|
||||
|
||||
if (!this.videoList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = async (event) => {
|
||||
const trigger = event.target.closest(
|
||||
"[data-play-magnet],[data-play-url]"
|
||||
@@ -5527,7 +5595,9 @@ class bitvidApp {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.button === 0 && !event.ctrlKey && !event.metaKey) {
|
||||
const isPrimaryClick =
|
||||
typeof event.button !== "number" || event.button === 0;
|
||||
if (isPrimaryClick && !event.ctrlKey && !event.metaKey) {
|
||||
event.preventDefault();
|
||||
|
||||
const rawUrlValue =
|
||||
@@ -5560,9 +5630,9 @@ class bitvidApp {
|
||||
}
|
||||
};
|
||||
|
||||
this._videoListElement = this.videoList;
|
||||
nextTarget.addEventListener("click", handler);
|
||||
this._videoListElement = nextTarget;
|
||||
this._videoListClickHandler = handler;
|
||||
this.videoList.addEventListener("click", handler);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -794,7 +794,6 @@ async function loadUserVideos(pubkey) {
|
||||
app.handleUrlHealthBadge({ video, url, badgeEl });
|
||||
});
|
||||
|
||||
window.app.videoList = container;
|
||||
window.app.attachVideoListHandler();
|
||||
|
||||
// Lazy-load images
|
||||
|
||||
+308
-175
@@ -6,214 +6,347 @@ import { subscriptions } from "./subscriptions.js";
|
||||
|
||||
const DEFAULT_BATCH_SIZE = 20;
|
||||
const BATCH_SIZE = WATCH_HISTORY_BATCH_RESOLVE ? DEFAULT_BATCH_SIZE : 1;
|
||||
const EMPTY_COPY =
|
||||
|
||||
export const WATCH_HISTORY_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 toNumber(value, fallback) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
function getElements() {
|
||||
return {
|
||||
view: document.getElementById("watchHistoryView"),
|
||||
grid: document.getElementById("watchHistoryGrid"),
|
||||
loadingEl: document.getElementById("watchHistoryLoading"),
|
||||
emptyEl: document.getElementById("watchHistoryEmpty"),
|
||||
sentinel: document.getElementById("watchHistorySentinel"),
|
||||
export function createWatchHistoryRenderer(config = {}) {
|
||||
const {
|
||||
viewSelector = "#watchHistoryView",
|
||||
gridSelector = "#watchHistoryGrid",
|
||||
loadingSelector = "#watchHistoryLoading",
|
||||
emptySelector = "#watchHistoryEmpty",
|
||||
sentinelSelector = "#watchHistorySentinel",
|
||||
scrollContainerSelector = null,
|
||||
emptyCopy = WATCH_HISTORY_EMPTY_COPY,
|
||||
beforeInitialLoad = async () => {
|
||||
const actor = window.app?.pubkey || undefined;
|
||||
await nostrClient.fetchWatchHistory(actor);
|
||||
},
|
||||
resolveBatch = (size) => nostrClient.resolveWatchHistory(size),
|
||||
renderGrid = null,
|
||||
batchSize = BATCH_SIZE,
|
||||
} = config;
|
||||
|
||||
const selectors = {
|
||||
view: viewSelector,
|
||||
grid: gridSelector,
|
||||
loading: loadingSelector,
|
||||
empty: emptySelector,
|
||||
sentinel: sentinelSelector,
|
||||
scrollContainer: scrollContainerSelector,
|
||||
};
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
const state = {
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
observer: null,
|
||||
scrollListener: null,
|
||||
resolvedVideos: [],
|
||||
initialized: false,
|
||||
emptyCopy,
|
||||
batchSize: Math.max(1, toNumber(batchSize, BATCH_SIZE)),
|
||||
};
|
||||
|
||||
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);
|
||||
const query = (selector) => {
|
||||
if (!selector) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
nextVideos.forEach((video) => {
|
||||
if (video && video.id) {
|
||||
dedupeMap.set(video.id, video);
|
||||
try {
|
||||
return document.querySelector(selector);
|
||||
} catch (error) {
|
||||
console.warn("[historyView] Failed to query selector:", selector, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getElements = () => ({
|
||||
view: query(selectors.view),
|
||||
grid: query(selectors.grid),
|
||||
loadingEl: query(selectors.loading),
|
||||
emptyEl: query(selectors.empty),
|
||||
sentinel: query(selectors.sentinel),
|
||||
scrollContainer: query(selectors.scrollContainer),
|
||||
});
|
||||
resolvedVideos = Array.from(dedupeMap.values());
|
||||
}
|
||||
|
||||
function renderResolvedVideos() {
|
||||
const { grid, emptyEl } = getElements();
|
||||
if (!grid) {
|
||||
return;
|
||||
}
|
||||
const setLoadingVisible = (visible) => {
|
||||
const { loadingEl } = getElements();
|
||||
if (!loadingEl) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
loadingEl.classList.remove("hidden");
|
||||
} else {
|
||||
loadingEl.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
if (!resolvedVideos.length) {
|
||||
const 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.textContent = state.emptyCopy;
|
||||
emptyEl.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupObservers = () => {
|
||||
if (state.observer) {
|
||||
state.observer.disconnect();
|
||||
state.observer = null;
|
||||
}
|
||||
if (state.scrollListener?.target && state.scrollListener?.handler) {
|
||||
state.scrollListener.target.removeEventListener(
|
||||
"scroll",
|
||||
state.scrollListener.handler
|
||||
);
|
||||
state.scrollListener = null;
|
||||
}
|
||||
};
|
||||
|
||||
const showEmptyState = (message = state.emptyCopy) => {
|
||||
const { grid, emptyEl } = getElements();
|
||||
if (grid) {
|
||||
grid.innerHTML = "";
|
||||
grid.classList.add("hidden");
|
||||
}
|
||||
if (emptyEl) {
|
||||
emptyEl.textContent = message;
|
||||
emptyEl.classList.remove("hidden");
|
||||
}
|
||||
grid.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
setLoadingVisible(false);
|
||||
state.hasMore = false;
|
||||
cleanupObservers();
|
||||
};
|
||||
|
||||
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);
|
||||
const mergeResolvedVideos = (nextVideos) => {
|
||||
if (!Array.isArray(nextVideos) || !nextVideos.length) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
const dedupeMap = new Map();
|
||||
state.resolvedVideos.forEach((video) => {
|
||||
if (video && video.id) {
|
||||
dedupeMap.set(video.id, video);
|
||||
}
|
||||
});
|
||||
nextVideos.forEach((video) => {
|
||||
if (video && video.id) {
|
||||
dedupeMap.set(video.id, video);
|
||||
}
|
||||
});
|
||||
state.resolvedVideos = Array.from(dedupeMap.values());
|
||||
};
|
||||
|
||||
try {
|
||||
const videos = await nostrClient.resolveWatchHistory(BATCH_SIZE);
|
||||
const renderResolvedVideos = () => {
|
||||
const { grid, emptyEl } = getElements();
|
||||
if (!grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (videos.length === 0) {
|
||||
if (initial && resolvedVideos.length === 0) {
|
||||
showEmptyState();
|
||||
} else {
|
||||
hasMore = false;
|
||||
cleanupObservers();
|
||||
if (!state.resolvedVideos.length) {
|
||||
if (emptyEl) {
|
||||
emptyEl.textContent = state.emptyCopy;
|
||||
emptyEl.classList.remove("hidden");
|
||||
}
|
||||
grid.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof renderGrid === "function") {
|
||||
renderGrid(state.resolvedVideos, grid);
|
||||
} else if (grid.id) {
|
||||
subscriptions.renderSameGridStyle(state.resolvedVideos, grid.id);
|
||||
} else {
|
||||
subscriptions.renderSameGridStyle(state.resolvedVideos, "watchHistoryGrid");
|
||||
}
|
||||
|
||||
grid.classList.remove("hidden");
|
||||
if (emptyEl) {
|
||||
emptyEl.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
const loadNextBatch = async ({ initial = false } = {}) => {
|
||||
if (state.isLoading || !state.hasMore) {
|
||||
if (initial && !state.isLoading && state.resolvedVideos.length === 0) {
|
||||
setLoadingVisible(false);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
state.isLoading = true;
|
||||
|
||||
function attachObservers() {
|
||||
const { sentinel } = getElements();
|
||||
if (!sentinel || !hasMore) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const videos = await resolveBatch(state.batchSize);
|
||||
|
||||
if ("IntersectionObserver" in window) {
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
loadNextBatch();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "300px 0px" }
|
||||
);
|
||||
observer.observe(sentinel);
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(videos) || videos.length === 0) {
|
||||
if (initial && state.resolvedVideos.length === 0) {
|
||||
showEmptyState();
|
||||
} else {
|
||||
state.hasMore = false;
|
||||
cleanupObservers();
|
||||
setLoadingVisible(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
scrollHandler = () => {
|
||||
const rect = sentinel.getBoundingClientRect();
|
||||
if (rect.top <= window.innerHeight + 200) {
|
||||
loadNextBatch();
|
||||
mergeResolvedVideos(videos);
|
||||
renderResolvedVideos();
|
||||
setLoadingVisible(false);
|
||||
} catch (error) {
|
||||
console.error("[historyView] Failed to resolve watch history:", error);
|
||||
if (initial && state.resolvedVideos.length === 0) {
|
||||
showEmptyState(
|
||||
"We couldn't load your watch history. Please try again later."
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
state.isLoading = false;
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", scrollHandler, { passive: true });
|
||||
scrollHandler();
|
||||
|
||||
const attachObservers = () => {
|
||||
const { sentinel, scrollContainer } = getElements();
|
||||
if (!sentinel || !state.hasMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("IntersectionObserver" in window) {
|
||||
state.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
loadNextBatch();
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
root: scrollContainer || null,
|
||||
rootMargin: scrollContainer ? "200px 0px" : "300px 0px",
|
||||
}
|
||||
);
|
||||
state.observer.observe(sentinel);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = scrollContainer || window;
|
||||
const handler = () => {
|
||||
const rect = sentinel.getBoundingClientRect();
|
||||
const threshold = scrollContainer
|
||||
? scrollContainer.clientHeight
|
||||
: window.innerHeight;
|
||||
if (rect.top <= threshold + 200) {
|
||||
loadNextBatch();
|
||||
}
|
||||
};
|
||||
|
||||
target.addEventListener("scroll", handler, { passive: true });
|
||||
state.scrollListener = { target, handler };
|
||||
handler();
|
||||
};
|
||||
|
||||
const runInitialLoad = async () => {
|
||||
const { view } = getElements();
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanupObservers();
|
||||
state.resolvedVideos = [];
|
||||
state.hasMore = true;
|
||||
state.isLoading = false;
|
||||
state.initialized = false;
|
||||
resetUiState();
|
||||
|
||||
try {
|
||||
await beforeInitialLoad?.();
|
||||
} 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 (state.hasMore) {
|
||||
attachObservers();
|
||||
}
|
||||
|
||||
state.initialized = true;
|
||||
};
|
||||
|
||||
return {
|
||||
async init() {
|
||||
await runInitialLoad();
|
||||
},
|
||||
async ensureInitialLoad() {
|
||||
const { view } = getElements();
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.initialized) {
|
||||
await runInitialLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.resolvedVideos.length && !state.isLoading) {
|
||||
await loadNextBatch({ initial: true });
|
||||
}
|
||||
},
|
||||
async loadMore() {
|
||||
await loadNextBatch();
|
||||
},
|
||||
resume() {
|
||||
cleanupObservers();
|
||||
if (state.hasMore) {
|
||||
attachObservers();
|
||||
}
|
||||
},
|
||||
pause() {
|
||||
cleanupObservers();
|
||||
},
|
||||
destroy() {
|
||||
cleanupObservers();
|
||||
state.initialized = false;
|
||||
state.resolvedVideos = [];
|
||||
state.hasMore = true;
|
||||
state.isLoading = false;
|
||||
resetUiState();
|
||||
setLoadingVisible(false);
|
||||
},
|
||||
render() {
|
||||
renderResolvedVideos();
|
||||
},
|
||||
getState() {
|
||||
return {
|
||||
...state,
|
||||
resolvedVideos: state.resolvedVideos.slice(),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const watchHistoryRenderer = createWatchHistoryRenderer();
|
||||
|
||||
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();
|
||||
}
|
||||
await watchHistoryRenderer.init();
|
||||
}
|
||||
|
||||
// Expose init on window for debugging/manual triggers if needed.
|
||||
if (!window.bitvid) {
|
||||
window.bitvid = {};
|
||||
}
|
||||
|
||||
@@ -378,7 +378,6 @@ function handleHashChange() {
|
||||
if (typeof initFn === "function") {
|
||||
initFn();
|
||||
}
|
||||
recordView("most-recent-videos");
|
||||
});
|
||||
});
|
||||
return;
|
||||
|
||||
+321
-123
@@ -687,6 +687,7 @@ class NostrClient {
|
||||
this.sessionActor = null;
|
||||
this.watchHistoryCache = new Map();
|
||||
this.watchHistoryStorage = null;
|
||||
this.watchHistoryRepublishTimers = new Map();
|
||||
}
|
||||
|
||||
restoreLocalData() {
|
||||
@@ -934,6 +935,206 @@ class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
cancelWatchHistoryRepublish(actor) {
|
||||
const key =
|
||||
typeof actor === "string" && actor.trim()
|
||||
? actor.trim().toLowerCase()
|
||||
: "";
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = this.watchHistoryRepublishTimers.get(key);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.watchHistoryRepublishTimers.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
scheduleWatchHistoryRepublish(actor, items) {
|
||||
if (!this.pool) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key =
|
||||
typeof actor === "string" && actor.trim()
|
||||
? actor.trim().toLowerCase()
|
||||
: "";
|
||||
if (!key || this.watchHistoryRepublishTimers.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clonedItems = Array.isArray(items)
|
||||
? items
|
||||
.map((item) => clonePointerItem(item))
|
||||
.filter((candidate) => !!candidate)
|
||||
: [];
|
||||
|
||||
if (!clonedItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.watchHistoryRepublishTimers.delete(key);
|
||||
this.publishWatchHistorySnapshot(actor, clonedItems, this.watchHistoryCache.get(actor) || null, {
|
||||
allowRetry: false,
|
||||
}).catch((error) => {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
`[nostr] Failed to republish watch history list for ${key}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
this.watchHistoryRepublishTimers.set(key, timer);
|
||||
}
|
||||
|
||||
async publishWatchHistorySnapshot(
|
||||
actorPubkey,
|
||||
candidateItems,
|
||||
existingEntry = null,
|
||||
options = {}
|
||||
) {
|
||||
if (!this.pool) {
|
||||
return { ok: false, error: "nostr-uninitialized" };
|
||||
}
|
||||
|
||||
const normalizedActor =
|
||||
typeof actorPubkey === "string" && actorPubkey.trim()
|
||||
? actorPubkey.trim()
|
||||
: "";
|
||||
if (!normalizedActor) {
|
||||
return { ok: false, error: "missing-actor" };
|
||||
}
|
||||
|
||||
const allowRetry = options?.allowRetry !== false;
|
||||
|
||||
const dedupe = new Map();
|
||||
const normalizedItems = [];
|
||||
const pushItem = (item) => {
|
||||
const pointer = normalizePointerInput(item);
|
||||
if (!pointer) {
|
||||
return;
|
||||
}
|
||||
const key = pointerKey(pointer);
|
||||
if (!key || dedupe.has(key)) {
|
||||
return;
|
||||
}
|
||||
dedupe.set(key, true);
|
||||
normalizedItems.push(pointer);
|
||||
};
|
||||
|
||||
if (Array.isArray(candidateItems)) {
|
||||
candidateItems.forEach((item) => pushItem(item));
|
||||
} else if (candidateItems) {
|
||||
pushItem(candidateItems);
|
||||
}
|
||||
|
||||
const trimmedItems =
|
||||
normalizedItems.length > WATCH_HISTORY_MAX_ITEMS
|
||||
? normalizedItems.slice(0, WATCH_HISTORY_MAX_ITEMS)
|
||||
: normalizedItems;
|
||||
|
||||
const tags = [["d", WATCH_HISTORY_LIST_IDENTIFIER]];
|
||||
for (const item of trimmedItems) {
|
||||
const tag = pointerToTag(item);
|
||||
if (tag) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: WATCH_HISTORY_KIND,
|
||||
pubkey: normalizedActor,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
|
||||
let signedEvent;
|
||||
const normalizedLogged =
|
||||
typeof this.pubkey === "string" ? this.pubkey.toLowerCase() : "";
|
||||
|
||||
if (
|
||||
normalizedActor.toLowerCase() === normalizedLogged &&
|
||||
window?.nostr &&
|
||||
typeof window.nostr.signEvent === "function"
|
||||
) {
|
||||
try {
|
||||
signedEvent = await window.nostr.signEvent(event);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[nostr] Failed to sign watch history list with extension:",
|
||||
error
|
||||
);
|
||||
return { ok: false, error: "signing-failed", details: error };
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (!this.sessionActor || this.sessionActor.pubkey !== normalizedActor) {
|
||||
await this.ensureSessionActor(true);
|
||||
}
|
||||
if (!this.sessionActor || this.sessionActor.pubkey !== normalizedActor) {
|
||||
throw new Error("session-actor-mismatch");
|
||||
}
|
||||
signedEvent = signEventWithPrivateKey(
|
||||
event,
|
||||
this.sessionActor.privateKey
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[nostr] Failed to sign watch history list with session key:",
|
||||
error
|
||||
);
|
||||
return { ok: false, error: "signing-failed", details: error };
|
||||
}
|
||||
}
|
||||
|
||||
const relays =
|
||||
Array.isArray(this.relays) && this.relays.length ? this.relays : RELAY_URLS;
|
||||
|
||||
const publishResults = await Promise.all(
|
||||
relays.map((url) => publishEventToRelay(this.pool, url, signedEvent))
|
||||
);
|
||||
|
||||
const success = publishResults.some((result) => result.success);
|
||||
if (!success) {
|
||||
console.warn(
|
||||
"[nostr] Failed to publish watch history list:",
|
||||
publishResults
|
||||
);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
this.cancelWatchHistoryRepublish(normalizedActor);
|
||||
} else if (allowRetry) {
|
||||
this.scheduleWatchHistoryRepublish(normalizedActor, trimmedItems);
|
||||
}
|
||||
|
||||
const baselineEntry =
|
||||
existingEntry || this.watchHistoryCache.get(normalizedActor) || null;
|
||||
|
||||
const newEntry = this.createWatchHistoryEntry(
|
||||
signedEvent,
|
||||
trimmedItems,
|
||||
Date.now(),
|
||||
baselineEntry
|
||||
);
|
||||
|
||||
this.watchHistoryCache.set(normalizedActor, newEntry);
|
||||
this.persistWatchHistoryEntry(normalizedActor, newEntry);
|
||||
|
||||
return {
|
||||
ok: success,
|
||||
event: signedEvent,
|
||||
items: newEntry.items
|
||||
.map((item) => clonePointerItem(item))
|
||||
.filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
async ensureSessionActor(forceSession = false) {
|
||||
const loggedInPubkey =
|
||||
typeof this.pubkey === "string" && this.pubkey.trim()
|
||||
@@ -979,10 +1180,19 @@ class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!window?.NostrTools?.generatePrivateKey ||
|
||||
typeof window.NostrTools.getPublicKey !== "function"
|
||||
) {
|
||||
const nostrTools = window?.NostrTools || {};
|
||||
const keyGenerator =
|
||||
typeof nostrTools.generatePrivateKey === "function"
|
||||
? nostrTools.generatePrivateKey
|
||||
: typeof nostrTools.generateSecretKey === "function"
|
||||
? nostrTools.generateSecretKey
|
||||
: null;
|
||||
const pubkeyDeriver =
|
||||
typeof nostrTools.getPublicKey === "function"
|
||||
? nostrTools.getPublicKey
|
||||
: null;
|
||||
|
||||
if (!keyGenerator || !pubkeyDeriver) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Unable to generate session actor: missing NostrTools helpers."
|
||||
@@ -991,8 +1201,69 @@ class NostrClient {
|
||||
return "";
|
||||
}
|
||||
|
||||
const privateKey = window.NostrTools.generatePrivateKey();
|
||||
const pubkey = window.NostrTools.getPublicKey(privateKey);
|
||||
let privateKey = keyGenerator();
|
||||
if (privateKey && typeof privateKey !== "string") {
|
||||
const hasLength =
|
||||
typeof privateKey === "object" &&
|
||||
privateKey !== null &&
|
||||
typeof privateKey.length === "number";
|
||||
if (nostrTools.utils?.bytesToHex && hasLength) {
|
||||
try {
|
||||
privateKey = nostrTools.utils.bytesToHex(privateKey);
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Failed to normalize generated private key:",
|
||||
error
|
||||
);
|
||||
}
|
||||
privateKey = "";
|
||||
}
|
||||
} else {
|
||||
privateKey = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof privateKey !== "string" || !privateKey) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Unable to generate session actor: invalid private key output."
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
let pubkey = pubkeyDeriver(privateKey);
|
||||
if (pubkey && typeof pubkey !== "string") {
|
||||
const hasLength =
|
||||
typeof pubkey === "object" &&
|
||||
pubkey !== null &&
|
||||
typeof pubkey.length === "number";
|
||||
if (nostrTools.utils?.bytesToHex && hasLength) {
|
||||
try {
|
||||
pubkey = nostrTools.utils.bytesToHex(pubkey);
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Failed to normalize generated public key:",
|
||||
error
|
||||
);
|
||||
}
|
||||
pubkey = "";
|
||||
}
|
||||
} else {
|
||||
pubkey = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof pubkey !== "string" || !pubkey) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Unable to generate session actor: invalid public key output."
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
this.sessionActor = { pubkey, privateKey };
|
||||
|
||||
@@ -1128,118 +1399,8 @@ class NostrClient {
|
||||
this.watchHistoryCache.get(actorPubkey) ||
|
||||
this.createWatchHistoryEntry(null, [], Date.now());
|
||||
|
||||
const dedupe = new Map();
|
||||
const nextItems = [];
|
||||
const pushPointer = (item) => {
|
||||
const candidate = normalizePointerInput(item);
|
||||
if (!candidate) {
|
||||
return;
|
||||
}
|
||||
const key = pointerKey(candidate);
|
||||
if (!key || dedupe.has(key)) {
|
||||
return;
|
||||
}
|
||||
dedupe.set(key, true);
|
||||
nextItems.push(candidate);
|
||||
};
|
||||
|
||||
pushPointer(normalizedPointer);
|
||||
for (const item of existingEntry.items) {
|
||||
pushPointer(item);
|
||||
if (nextItems.length >= WATCH_HISTORY_MAX_ITEMS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const trimmedItems =
|
||||
nextItems.length > WATCH_HISTORY_MAX_ITEMS
|
||||
? nextItems.slice(0, WATCH_HISTORY_MAX_ITEMS)
|
||||
: nextItems;
|
||||
|
||||
const tags = [["d", WATCH_HISTORY_LIST_IDENTIFIER]];
|
||||
for (const item of trimmedItems) {
|
||||
const tag = pointerToTag(item);
|
||||
if (tag) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: WATCH_HISTORY_KIND,
|
||||
pubkey: actorPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
|
||||
let signedEvent;
|
||||
const normalizedActor = actorPubkey.toLowerCase();
|
||||
const normalizedLogged =
|
||||
typeof this.pubkey === "string" ? this.pubkey.toLowerCase() : "";
|
||||
|
||||
if (
|
||||
normalizedActor &&
|
||||
normalizedActor === normalizedLogged &&
|
||||
window?.nostr &&
|
||||
typeof window.nostr.signEvent === "function"
|
||||
) {
|
||||
try {
|
||||
signedEvent = await window.nostr.signEvent(event);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[nostr] Failed to sign watch history list with extension:",
|
||||
error
|
||||
);
|
||||
return { ok: false, error: "signing-failed", details: error };
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (!this.sessionActor || this.sessionActor.pubkey !== actorPubkey) {
|
||||
await this.ensureSessionActor(true);
|
||||
}
|
||||
if (!this.sessionActor || this.sessionActor.pubkey !== actorPubkey) {
|
||||
throw new Error("session-actor-mismatch");
|
||||
}
|
||||
const privateKey = this.sessionActor.privateKey;
|
||||
signedEvent = signEventWithPrivateKey(event, privateKey);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[nostr] Failed to sign watch history list with session key:",
|
||||
error
|
||||
);
|
||||
return { ok: false, error: "signing-failed", details: error };
|
||||
}
|
||||
}
|
||||
|
||||
const relays =
|
||||
Array.isArray(this.relays) && this.relays.length ? this.relays : RELAY_URLS;
|
||||
|
||||
const publishResults = await Promise.all(
|
||||
relays.map((url) => publishEventToRelay(this.pool, url, signedEvent))
|
||||
);
|
||||
const success = publishResults.some((result) => result.success);
|
||||
if (!success) {
|
||||
console.warn(
|
||||
"[nostr] Failed to publish watch history list:",
|
||||
publishResults
|
||||
);
|
||||
}
|
||||
|
||||
const newEntry = this.createWatchHistoryEntry(
|
||||
signedEvent,
|
||||
trimmedItems,
|
||||
Date.now(),
|
||||
existingEntry
|
||||
);
|
||||
|
||||
this.watchHistoryCache.set(actorPubkey, newEntry);
|
||||
this.persistWatchHistoryEntry(actorPubkey, newEntry);
|
||||
|
||||
return {
|
||||
ok: success,
|
||||
event: signedEvent,
|
||||
items: newEntry.items.map((item) => clonePointerItem(item)).filter(Boolean),
|
||||
};
|
||||
const candidates = [normalizedPointer, ...(existingEntry.items || [])];
|
||||
return this.publishWatchHistorySnapshot(actorPubkey, candidates, existingEntry);
|
||||
}
|
||||
|
||||
async fetchWatchHistory(pubkeyOrSession) {
|
||||
@@ -1269,9 +1430,11 @@ class NostrClient {
|
||||
};
|
||||
}
|
||||
|
||||
let storage = null;
|
||||
let stored = null;
|
||||
if (typeof localStorage !== "undefined") {
|
||||
const storage = this.getWatchHistoryStorage();
|
||||
const stored = storage.actors?.[actor];
|
||||
storage = this.getWatchHistoryStorage();
|
||||
stored = storage.actors?.[actor];
|
||||
if (
|
||||
stored &&
|
||||
typeof stored.savedAt === "number" &&
|
||||
@@ -1354,17 +1517,48 @@ class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
const items = pointerEvent ? extractPointerItemsFromEvent(pointerEvent) : [];
|
||||
const previousEntry = this.watchHistoryCache.get(actor) || null;
|
||||
const remoteItems = pointerEvent
|
||||
? extractPointerItemsFromEvent(pointerEvent)
|
||||
: [];
|
||||
|
||||
let items = remoteItems;
|
||||
let pointerForEntry = pointerEvent;
|
||||
let usedFallback = false;
|
||||
|
||||
const storedItems = Array.isArray(stored?.items) ? stored.items : [];
|
||||
const storedPointerEvent = stored?.pointerEvent || null;
|
||||
|
||||
if (!pointerEvent && previousEntry?.pointerEvent) {
|
||||
pointerForEntry = previousEntry.pointerEvent;
|
||||
} else if (!pointerEvent && storedPointerEvent) {
|
||||
pointerForEntry = storedPointerEvent;
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
if (previousEntry?.items?.length) {
|
||||
items = previousEntry.items;
|
||||
usedFallback = true;
|
||||
} else if (storedItems.length) {
|
||||
items = storedItems;
|
||||
usedFallback = true;
|
||||
}
|
||||
}
|
||||
|
||||
const entry = this.createWatchHistoryEntry(
|
||||
pointerEvent,
|
||||
pointerForEntry,
|
||||
items,
|
||||
Date.now(),
|
||||
this.watchHistoryCache.get(actor) || null
|
||||
previousEntry
|
||||
);
|
||||
|
||||
this.watchHistoryCache.set(actor, entry);
|
||||
this.persistWatchHistoryEntry(actor, entry);
|
||||
|
||||
if (!pointerEvent && usedFallback && entry.items.length) {
|
||||
this.scheduleWatchHistoryRepublish(actor, entry.items);
|
||||
}
|
||||
|
||||
return {
|
||||
pointerEvent: entry.pointerEvent
|
||||
? cloneEventForCache(entry.pointerEvent)
|
||||
@@ -1711,6 +1905,10 @@ class NostrClient {
|
||||
|
||||
logout() {
|
||||
this.pubkey = null;
|
||||
for (const timer of this.watchHistoryRepublishTimers.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this.watchHistoryRepublishTimers.clear();
|
||||
if (isDevMode) console.log("User logged out.");
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -614,10 +614,7 @@ class SubscriptionsManager {
|
||||
window.app.handleUrlHealthBadge({ video, url, badgeEl });
|
||||
});
|
||||
|
||||
if (window.app) {
|
||||
window.app.videoList = container;
|
||||
window.app.attachVideoListHandler?.();
|
||||
}
|
||||
window.app?.attachVideoListHandler?.();
|
||||
|
||||
// Lazy-load
|
||||
const lazyEls = container.querySelectorAll("[data-lazy]");
|
||||
|
||||
Reference in new issue
Block a user