Refactor app orchestrator into Application with dependency injection

This commit is contained in:
thePR0M3TH3AN committed 2025-10-03 21:10:22 -04:00
1 parent ef6b8bf1cf
commit cf34e28a47
17 files changed
+525 -375

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# Application Responsibility Audit
This document catalogs the major responsibilities still implemented inside `Application` (formerly `bitvidApp`). Each area is a candidate for future extraction into dedicated service or UI modules.
## Auth & Profile Coordination
- Instantiates `AuthService`, wires login/logout/profile listeners, and exposes helper methods for cached profile state management.【F:js/app.js†L221-L314】【F:js/app.js†L620-L772】
- Manages profile modal lifecycle, navigation between panes, and account switching UI state.【F:js/app.js†L283-L420】【F:js/app.js†L1328-L1822】
## Playback Orchestration
- Configures `PlaybackService`, logs telemetry callbacks, and tracks the active playback session lifecycle.【F:js/app.js†L224-L323】【F:js/app.js†L2805-L3352】
- Coordinates view logging, cooldown keys, and watch-count subscriptions for the primary player and modal overlays.【F:js/app.js†L4305-L4592】【F:js/app.js†L4972-L5332】
## Modal & UI Composition
- Constructs upload, edit, revert, and video modals; registers their event listeners; and bridges UI events back into application logic.【F:js/app.js†L340-L615】【F:js/app.js†L2382-L2799】
- Builds and manages the `VideoListView` component, injecting helpers for health badges, share links, and context menu actions.【F:js/app.js†L516-L744】【F:js/app.js†L2149-L2376】
## Routing & View Bootstrapping
- Handles initial view loading, hash-based navigation, and video list mounting during application startup.【F:js/app.js†L903-L959】【F:js/app.js†L1932-L2124】
- Provides navigation helpers for channel/profile transitions tied to hash routing.【F:js/app.js†L934-L968】
## State & Watch History Management
- Synchronizes watch-history preferences, local metadata caching, and publication of watch history events to services.【F:js/app.js†L1975-L2093】【F:js/app.js†L4087-L4478】
- Tracks app-level state such as active intervals, cleanup routines, view counters, and URL/magnet health caches.【F:js/app.js†L200-L323】【F:js/app.js†L3354-L4075】
These clusters outline the remaining cross-cutting responsibilities that can be incrementally migrated into focused modules under `js/services` and `js/ui`.
+1 -1
View File
@@ -227,7 +227,7 @@
<script type="module" src="js/webtorrent.js"></script>
<script type="module" src="js/nostr.js"></script>
<script type="module" src="js/viewManager.js"></script>
<script type="module" src="js/app.js"></script>
<script type="module" src="js/bootstrap.js"></script>
<script type="module" src="js/disclaimer.js"></script>
<script type="module" src="js/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
+147 -293
View File
@@ -1,6 +1,5 @@
// js/app.js
import { loadView } from "./viewManager.js";
import {
nostrClient,
normalizePointerInput,
@@ -48,6 +47,11 @@ import { UploadModal } from "./ui/components/UploadModal.js";
import { EditModal } from "./ui/components/EditModal.js";
import { RevertModal } from "./ui/components/RevertModal.js";
import { VideoListView } from "./ui/views/VideoListView.js";
import { MediaLoader } from "./utils/mediaLoader.js";
import { pointerArrayToKey } from "./utils/pointer.js";
import { fakeDecrypt } from "./utils/fakeDecrypt.js";
import { isValidMagnetUri } from "./utils/magnetValidators.js";
import { dedupeToNewestByRoot } from "./utils/videoDeduper.js";
import {
getPubkey as getStoredPubkey,
setPubkey as setStoredPubkey,
@@ -73,37 +77,6 @@ import {
urlHealthConstants,
} from "./state/cache.js";
function pointerArrayToKey(pointer) {
if (!Array.isArray(pointer) || pointer.length < 2) {
return "";
}
const type = pointer[0] === "a" ? "a" : pointer[0] === "e" ? "e" : "";
if (!type) {
return "";
}
const value =
typeof pointer[1] === "string" ? pointer[1].trim().toLowerCase() : "";
if (!value) {
return "";
}
const relay =
pointer.length > 2 && typeof pointer[2] === "string"
? pointer[2].trim()
: "";
return relay ? `${type}:${value}:${relay}` : `${type}:${value}`;
}
/**
* Simple "decryption" placeholder for private videos.
*/
function fakeDecrypt(str) {
return str.split("").reverse().join("");
}
const UNSUPPORTED_BTITH_MESSAGE =
"This magnet link is missing a compatible BitTorrent v1 info hash.";
@@ -178,143 +151,7 @@ function isValidMagnetUri(magnet) {
* This will load the real image source from `imgElement.dataset.lazy`
* once the image enters the viewport.
*/
class MediaLoader {
constructor(rootMargin = "50px") {
this.observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const el = entry.target;
const lazySrc =
typeof el.dataset.lazy === "string"
? el.dataset.lazy.trim()
: "";
if (lazySrc) {
const fallbackSrc =
(typeof el.dataset.fallbackSrc === "string"
? el.dataset.fallbackSrc.trim()
: "") ||
el.getAttribute("data-fallback-src") ||
"";
const applyFallback = () => {
const resolvedFallback =
fallbackSrc && fallbackSrc.trim()
? fallbackSrc.trim()
: FALLBACK_THUMBNAIL_SRC;
if (!resolvedFallback) {
return;
}
if (el.src !== resolvedFallback) {
el.src = resolvedFallback;
}
if (!el.dataset.fallbackSrc) {
el.dataset.fallbackSrc = resolvedFallback;
}
if (!el.getAttribute("data-fallback-src")) {
el.setAttribute("data-fallback-src", resolvedFallback);
}
el.dataset.thumbnailFailed = "true";
};
const cleanupListeners = () => {
el.removeEventListener("error", handleError);
el.removeEventListener("load", handleLoad);
};
const handleError = () => {
cleanupListeners();
applyFallback();
};
const handleLoad = () => {
cleanupListeners();
if (
(el.naturalWidth === 0 && el.naturalHeight === 0) ||
!el.currentSrc
) {
applyFallback();
} else {
delete el.dataset.thumbnailFailed;
}
};
el.addEventListener("error", handleError);
el.addEventListener("load", handleLoad);
if (fallbackSrc && lazySrc === fallbackSrc) {
cleanupListeners();
delete el.dataset.thumbnailFailed;
} else {
el.src = lazySrc;
if (el.complete) {
// Handle cached results synchronously
if (el.naturalWidth === 0 && el.naturalHeight === 0) {
handleError();
} else {
handleLoad();
}
}
}
}
delete el.dataset.lazy;
// Stop observing once loaded
this.observer.unobserve(el);
}
}
},
{ rootMargin }
);
}
observe(el) {
if (!el || typeof el.dataset === "undefined") {
return;
}
if (!el.dataset.fallbackSrc) {
const fallbackAttr =
el.getAttribute("data-fallback-src") || el.getAttribute("src") || "";
if (fallbackAttr) {
el.dataset.fallbackSrc = fallbackAttr;
} else if (el.tagName === "IMG") {
el.dataset.fallbackSrc = FALLBACK_THUMBNAIL_SRC;
el.setAttribute("data-fallback-src", FALLBACK_THUMBNAIL_SRC);
}
}
if (
el.tagName === "IMG" &&
typeof HTMLImageElement !== "undefined" &&
"loading" in HTMLImageElement.prototype
) {
el.loading = el.loading || "lazy";
if ("decoding" in HTMLImageElement.prototype) {
el.decoding = el.decoding || "async";
}
}
const lazySrc =
typeof el.dataset.lazy === "string" ? el.dataset.lazy.trim() : "";
if (lazySrc) {
this.observer.observe(el);
}
}
disconnect() {
this.observer.disconnect();
}
}
class bitvidApp {
class Application {
get pubkey() {
return getStoredPubkey();
}
@@ -339,15 +176,23 @@ class bitvidApp {
setStoredCurrentVideo(value ?? null);
}
constructor() {
constructor({ services = {}, ui = {}, helpers = {}, loadView: viewLoader } = {}) {
this.loadView = typeof viewLoader === "function" ? viewLoader : null;
// Basic auth/display elements
this.loginButton = document.getElementById("loginButton") || null;
this.logoutButton = document.getElementById("logoutButton") || null;
this.userStatus = document.getElementById("userStatus") || null;
this.userPubKey = document.getElementById("userPubKey") || null;
// Lazy-loading helper for images
this.mediaLoader = new MediaLoader();
const mediaLoaderFactory =
typeof helpers.mediaLoaderFactory === "function"
? helpers.mediaLoaderFactory
: () => new MediaLoader();
this.mediaLoader =
helpers.mediaLoader instanceof MediaLoader
? helpers.mediaLoader
: mediaLoaderFactory();
this.loadedThumbnails = new Map();
this.videoDiscussionCountCache = new Map();
this.inFlightDiscussionCounts = new Map();
@@ -373,12 +218,17 @@ class bitvidApp {
this.boundVideoListShareListener = null;
this.boundVideoListContextListener = null;
this.playbackService = new PlaybackService({
logger: (message, ...args) => this.log(message, ...args),
torrentClient,
deriveTorrentPlaybackConfig,
isValidMagnetUri,
urlFirstEnabled: URL_FIRST_ENABLED,
this.nostrService = services.nostrService || nostrService;
this.r2Service = services.r2Service || r2Service;
this.playbackService =
services.playbackService ||
new PlaybackService({
logger: (message, ...args) => this.log(message, ...args),
torrentClient,
deriveTorrentPlaybackConfig,
isValidMagnetUri,
urlFirstEnabled: URL_FIRST_ENABLED,
analyticsCallbacks: {
"session-start": (detail) => {
const urlProvided = detail?.urlProvided ? "true" : "false";
@@ -401,15 +251,17 @@ class bitvidApp {
}
},
},
});
});
this.activePlaybackSession = null;
this.authService = new AuthService({
nostrClient,
userBlocks,
relayManager,
logger: (message, ...args) => this.log(message, ...args),
});
this.authService =
services.authService ||
new AuthService({
nostrClient,
userBlocks,
relayManager,
logger: (message, ...args) => this.log(message, ...args),
});
this.authEventUnsubscribes.push(
this.authService.on("auth:login", (detail) => this.handleAuthLogin(detail))
);
@@ -487,22 +339,25 @@ class bitvidApp {
// Upload modal component
this.uploadButton = document.getElementById("uploadButton") || null;
this.r2Service = r2Service;
const uploadModalEvents = new EventTarget();
this.uploadModal = new UploadModal({
authService: this.authService,
r2Service: this.r2Service,
publishVideoNote: (payload, options) =>
this.publishVideoNote(payload, options),
removeTrackingScripts,
setGlobalModalState,
showError: (message) => this.showError(message),
showSuccess: (message) => this.showSuccess(message),
getCurrentPubkey: () => this.pubkey,
safeEncodeNpub: (pubkey) => this.safeEncodeNpub(pubkey),
eventTarget: uploadModalEvents,
container: document.getElementById("modalContainer") || null,
});
this.uploadModal =
(typeof ui.uploadModal === "function"
? ui.uploadModal({ app: this, eventTarget: uploadModalEvents })
: ui.uploadModal) ||
new UploadModal({
authService: this.authService,
r2Service: this.r2Service,
publishVideoNote: (payload, options) =>
this.publishVideoNote(payload, options),
removeTrackingScripts,
setGlobalModalState,
showError: (message) => this.showError(message),
showSuccess: (message) => this.showSuccess(message),
getCurrentPubkey: () => this.pubkey,
safeEncodeNpub: (pubkey) => this.safeEncodeNpub(pubkey),
eventTarget: uploadModalEvents,
container: document.getElementById("modalContainer") || null,
});
this.boundUploadSubmitHandler = (event) => {
this.handleUploadSubmitEvent(event);
};
@@ -512,21 +367,25 @@ class bitvidApp {
);
const editModalEvents = new EventTarget();
this.editModal = new EditModal({
removeTrackingScripts,
setGlobalModalState,
showError: (message) => this.showError(message),
getMode: () => (isDevMode ? "dev" : "live"),
sanitizers: {
text: (value) => (typeof value === "string" ? value.trim() : ""),
url: (value) => (typeof value === "string" ? value.trim() : ""),
magnet: (value) => (typeof value === "string" ? value.trim() : ""),
checkbox: (value) => !!value,
},
escapeHtml: (value) => escapeHtml(value),
eventTarget: editModalEvents,
container: document.getElementById("modalContainer") || null,
});
this.editModal =
(typeof ui.editModal === "function"
? ui.editModal({ app: this, eventTarget: editModalEvents })
: ui.editModal) ||
new EditModal({
removeTrackingScripts,
setGlobalModalState,
showError: (message) => this.showError(message),
getMode: () => (isDevMode ? "dev" : "live"),
sanitizers: {
text: (value) => (typeof value === "string" ? value.trim() : ""),
url: (value) => (typeof value === "string" ? value.trim() : ""),
magnet: (value) => (typeof value === "string" ? value.trim() : ""),
checkbox: (value) => !!value,
},
escapeHtml: (value) => escapeHtml(value),
eventTarget: editModalEvents,
container: document.getElementById("modalContainer") || null,
});
this.boundEditModalSubmitHandler = (event) => {
this.handleEditModalSubmit(event);
@@ -544,17 +403,21 @@ class bitvidApp {
this.boundEditModalCancelHandler
);
this.revertModal = new RevertModal({
removeTrackingScripts,
setGlobalModalState,
formatAbsoluteTimestamp: (timestamp) =>
this.formatAbsoluteTimestamp(timestamp),
formatTimeAgo: (timestamp) => this.formatTimeAgo(timestamp),
escapeHTML: (value) => this.escapeHTML(value),
truncateMiddle,
fallbackThumbnailSrc: FALLBACK_THUMBNAIL_SRC,
container: document.getElementById("modalContainer") || null,
});
this.revertModal =
(typeof ui.revertModal === "function"
? ui.revertModal({ app: this })
: ui.revertModal) ||
new RevertModal({
removeTrackingScripts,
setGlobalModalState,
formatAbsoluteTimestamp: (timestamp) =>
this.formatAbsoluteTimestamp(timestamp),
formatTimeAgo: (timestamp) => this.formatTimeAgo(timestamp),
escapeHTML: (value) => this.escapeHTML(value),
truncateMiddle,
fallbackThumbnailSrc: FALLBACK_THUMBNAIL_SRC,
container: document.getElementById("modalContainer") || null,
});
this.boundRevertConfirmHandler = (event) => {
this.handleRevertModalConfirm(event);
@@ -574,14 +437,18 @@ class bitvidApp {
this.cleanupPromise = null;
this.videoModal = new VideoModal({
removeTrackingScripts,
setGlobalModalState,
document,
logger: {
log: (message, ...args) => this.log(message, ...args),
},
});
this.videoModal =
(typeof ui.videoModal === "function"
? ui.videoModal({ app: this })
: ui.videoModal) ||
new VideoModal({
removeTrackingScripts,
setGlobalModalState,
document,
logger: {
log: (message, ...args) => this.log(message, ...args),
},
});
this.boundVideoModalCloseHandler = () => {
this.hideModal();
};
@@ -636,13 +503,13 @@ class bitvidApp {
this.currentVideoPointerKey = null;
this.playbackTelemetryState = null;
this.loggedViewPointerKeys = new Set();
this.videoSubscription = nostrService.getVideoSubscription() || null;
this.videoSubscription = this.nostrService.getVideoSubscription() || null;
this.videoList = null;
this.modalViewCountUnsub = null;
// Videos stored as a Map (key=event.id)
this.videosMap = nostrService.getVideosMap();
this.unsubscribeFromNostrService = nostrService.on(
this.videosMap = this.nostrService.getVideosMap();
this.unsubscribeFromNostrService = this.nostrService.on(
"subscription:changed",
({ subscription }) => {
this.videoSubscription = subscription || null;
@@ -684,7 +551,7 @@ class bitvidApp {
},
});
this.videoListView = new VideoListView({
const videoListViewConfig = {
document,
mediaLoader: this.mediaLoader,
badgeHelpers: { attachHealthBadges, attachUrlHealthBadges },
@@ -729,7 +596,12 @@ class bitvidApp {
renderers: {
getLoadingMarkup: (message) => getSidebarLoadingMarkup(message),
},
});
};
this.videoListView =
(typeof ui.videoListView === "function"
? ui.videoListView({ app: this, config: videoListViewConfig })
: ui.videoListView) ||
new VideoListView(videoListViewConfig);
this.videoListViewPlaybackHandler = ({ videoId, url, magnet }) => {
if (videoId) {
@@ -833,7 +705,7 @@ class bitvidApp {
}
} catch (err) {
console.error(
"[bitvidApp] Invalid nevent in blacklist:",
"[Application] Invalid nevent in blacklist:",
neventStr,
err
);
@@ -933,6 +805,11 @@ class bitvidApp {
async init() {
try {
if (typeof this.loadView !== "function") {
const module = await import("./viewManager.js");
this.loadView = module?.loadView || null;
}
// Force update of any registered service workers to ensure latest code is used.
if ("serviceWorker" in navigator) {
navigator.serviceWorker.getRegistrations().then((registrations) => {
@@ -1028,7 +905,9 @@ class bitvidApp {
console.log(
"[app.init()] No #view= in the URL, loading default home view"
);
await loadView("views/most-recent-videos.html");
if (typeof this.loadView === "function") {
await this.loadView("views/most-recent-videos.html");
}
} else {
console.log(
"[app.init()] Found hash:",
@@ -1636,7 +1515,7 @@ class bitvidApp {
) {
return nostrClient.sessionActor.pubkey;
}
return window.app?.pubkey || undefined;
return this.pubkey || undefined;
},
});
}
@@ -3661,7 +3540,7 @@ class bitvidApp {
}
try {
await nostrService.publishVideoNote(formData, this.pubkey);
await this.nostrService.publishVideoNote(formData, this.pubkey);
if (typeof onSuccess === "function") {
await onSuccess();
}
@@ -3959,8 +3838,8 @@ class bitvidApp {
}
if (!preserveSubscriptions) {
nostrService.clearVideoSubscription();
this.videoSubscription = nostrService.getVideoSubscription() || null;
this.nostrService.clearVideoSubscription();
this.videoSubscription = this.nostrService.getVideoSubscription() || null;
}
// If there's a small inline player
@@ -5028,7 +4907,7 @@ class bitvidApp {
container.innerHTML = getSidebarLoadingMarkup("Fetching recent videos…");
}
const videos = await nostrService.loadVideos({
const videos = await this.nostrService.loadVideos({
forceFetch,
blacklistedEventIds: this.blacklistedEventIds,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
@@ -5039,15 +4918,15 @@ class bitvidApp {
this.renderVideoList([]);
}
this.videoSubscription = nostrService.getVideoSubscription() || null;
this.videosMap = nostrService.getVideosMap();
this.videoSubscription = this.nostrService.getVideoSubscription() || null;
this.videosMap = this.nostrService.getVideosMap();
if (this.videoListView) {
this.videoListView.state.videosMap = this.videosMap;
}
}
async loadOlderVideos(lastTimestamp) {
const olderVideos = await nostrService.loadOlderVideos(lastTimestamp, {
const olderVideos = await this.nostrService.loadOlderVideos(lastTimestamp, {
blacklistedEventIds: this.blacklistedEventIds,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
});
@@ -5057,7 +4936,7 @@ class bitvidApp {
return;
}
const all = nostrService.getFilteredActiveVideos({
const all = this.nostrService.getFilteredActiveVideos({
blacklistedEventIds: this.blacklistedEventIds,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
});
@@ -6147,7 +6026,7 @@ class bitvidApp {
}
try {
await nostrService.handleEditVideoSubmit({
await this.nostrService.handleEditVideoSubmit({
originalEvent,
updatedData,
pubkey: this.pubkey,
@@ -6166,7 +6045,7 @@ class bitvidApp {
async handleEditVideo(target) {
try {
const normalizedTarget = this.normalizeActionTarget(target);
const latestVideos = await nostrService.fetchVideos({
const latestVideos = await this.nostrService.fetchVideos({
blacklistedEventIds: this.blacklistedEventIds,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
});
@@ -6210,7 +6089,7 @@ class bitvidApp {
async handleRevertVideo(target) {
try {
const normalizedTarget = this.normalizeActionTarget(target);
const activeVideos = await nostrService.fetchVideos({
const activeVideos = await this.nostrService.fetchVideos({
blacklistedEventIds: this.blacklistedEventIds,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
});
@@ -6306,7 +6185,7 @@ class bitvidApp {
async handleFullDeleteVideo(target) {
try {
const normalizedTarget = this.normalizeActionTarget(target);
const all = nostrService.getFilteredActiveVideos({
const all = this.nostrService.getFilteredActiveVideos({
blacklistedEventIds: this.blacklistedEventIds,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
});
@@ -6337,7 +6216,7 @@ class bitvidApp {
// We assume video.videoRootId is not empty, or fallback to video.id if needed
const rootId = video.videoRootId || video.id;
await nostrService.handleFullDeleteVideo({
await this.nostrService.handleFullDeleteVideo({
videoRootId: rootId,
pubkey: this.pubkey,
confirm: false,
@@ -7233,7 +7112,7 @@ class bitvidApp {
* this.videosMap or from a bulk fetch. Uses nostrClient.getEventById.
*/
async getOldEventById(eventId) {
return nostrService.getOldEventById(eventId);
return this.nostrService.getOldEventById(eventId);
}
/**
@@ -7319,7 +7198,7 @@ class bitvidApp {
try {
this.unsubscribeFromPubkeyState();
} catch (error) {
console.warn("[bitvidApp] Failed to unsubscribe pubkey state:", error);
console.warn("[Application] Failed to unsubscribe pubkey state:", error);
}
this.unsubscribeFromPubkeyState = null;
}
@@ -7329,7 +7208,7 @@ class bitvidApp {
this.unsubscribeFromCurrentUserState();
} catch (error) {
console.warn(
"[bitvidApp] Failed to unsubscribe current user state:",
"[Application] Failed to unsubscribe current user state:",
error
);
}
@@ -7341,7 +7220,7 @@ class bitvidApp {
this.watchHistoryPreferenceUnsubscribe();
} catch (error) {
console.warn(
"[bitvidApp] Failed to unsubscribe watch history preference:",
"[Application] Failed to unsubscribe watch history preference:",
error
);
}
@@ -7353,7 +7232,7 @@ class bitvidApp {
try {
unsubscribe();
} catch (error) {
console.warn("[bitvidApp] Auth listener unsubscribe failed:", error);
console.warn("[Application] Auth listener unsubscribe failed:", error);
}
}
});
@@ -7363,7 +7242,7 @@ class bitvidApp {
try {
this.unsubscribeFromNostrService();
} catch (error) {
console.warn("[bitvidApp] Failed to unsubscribe nostr service:", error);
console.warn("[Application] Failed to unsubscribe nostr service:", error);
}
this.unsubscribeFromNostrService = null;
}
@@ -7379,7 +7258,7 @@ class bitvidApp {
try {
this.uploadModal.destroy();
} catch (error) {
console.warn("[bitvidApp] Failed to destroy upload modal:", error);
console.warn("[Application] Failed to destroy upload modal:", error);
}
}
@@ -7402,7 +7281,7 @@ class bitvidApp {
try {
this.editModal.destroy();
} catch (error) {
console.warn("[bitvidApp] Failed to destroy edit modal:", error);
console.warn("[Application] Failed to destroy edit modal:", error);
}
}
}
@@ -7418,7 +7297,7 @@ class bitvidApp {
try {
this.revertModal.destroy();
} catch (error) {
console.warn("[bitvidApp] Failed to destroy revert modal:", error);
console.warn("[Application] Failed to destroy revert modal:", error);
}
}
@@ -7462,7 +7341,7 @@ class bitvidApp {
try {
this.videoModal.destroy();
} catch (error) {
console.warn("[bitvidApp] Failed to destroy video modal:", error);
console.warn("[Application] Failed to destroy video modal:", error);
}
}
}
@@ -7495,7 +7374,7 @@ class bitvidApp {
try {
this.videoListView.destroy();
} catch (error) {
console.warn("[bitvidApp] Failed to destroy VideoListView:", error);
console.warn("[Application] Failed to destroy VideoListView:", error);
}
}
@@ -7552,30 +7431,5 @@ class bitvidApp {
* return only the newest (by created_at) for each videoRootId.
* If no videoRootId is present, treat the video’s own ID as its root.
*/
function dedupeToNewestByRoot(videos) {
const map = new Map(); // key = rootId, value = newest video for that root
for (const vid of videos) {
// If there's no videoRootId, fall back to vid.id (treat it as its own "root")
const rootId = vid.videoRootId || vid.id;
const existing = map.get(rootId);
if (!existing || vid.created_at > existing.created_at) {
map.set(rootId, vid);
}
}
// Return just the newest from each group
return Array.from(map.values());
}
export const app = new bitvidApp();
export const appReady = app.init();
if (typeof window !== "undefined") {
window.app = app;
window.appReady = appReady;
window.bitvid = window.bitvid || {};
window.bitvid.app = app;
window.bitvid.appReady = appReady;
}
export { Application };
export default Application;
+24
View File
@@ -0,0 +1,24 @@
// js/applicationContext.js
let applicationInstance = null;
let readyPromise = Promise.resolve();
export function setApplication(app) {
applicationInstance = app || null;
}
export function getApplication() {
return applicationInstance;
}
export function setApplicationReady(promise) {
if (promise && typeof promise.then === "function") {
readyPromise = promise;
} else {
readyPromise = Promise.resolve(promise);
}
}
export function getApplicationReady() {
return readyPromise;
}
+23
View File
@@ -0,0 +1,23 @@
// js/bootstrap.js
import Application from "./app.js";
import nostrService from "./services/nostrService.js";
import r2Service from "./services/r2Service.js";
import { loadView } from "./viewManager.js";
import { setApplication, setApplicationReady } from "./applicationContext.js";
const app = new Application({
services: {
nostrService,
r2Service,
},
loadView,
});
setApplication(app);
const appReady = app.init();
setApplicationReady(appReady);
export { app, appReady };
export default app;
+32 -18
View File
@@ -4,12 +4,14 @@ import {
nostrClient,
convertEventToVideo as sharedConvertEventToVideo,
} from "./nostr.js";
import { app } from "./app.js";
import { subscriptions } from "./subscriptions.js"; // <-- NEW import
import { attachHealthBadges } from "./gridHealth.js";
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
import { accessControl } from "./accessControl.js";
import { escapeHTML } from "./utils/domUtils.js";
import { getApplication } from "./applicationContext.js";
const getApp = () => getApplication();
let currentChannelHex = null;
let currentChannelNpub = null;
@@ -70,7 +72,9 @@ function buildChannelShareUrl() {
return "";
}
const base = typeof app.getShareUrlBase === "function" ? app.getShareUrlBase() : "";
const app = getApp();
const base =
typeof app?.getShareUrlBase === "function" ? app.getShareUrlBase() : "";
if (base) {
return `${base}#view=channel-profile&npub=${encodeURIComponent(currentChannelNpub)}`;
}
@@ -109,6 +113,8 @@ function setupChannelShareButton() {
return;
}
const app = getApp();
syncChannelShareButtonState();
if (shareBtn.dataset.initialized === "true") {
@@ -118,14 +124,14 @@ function setupChannelShareButton() {
shareBtn.addEventListener("click", () => {
const shareUrl = buildChannelShareUrl();
if (!shareUrl) {
app.showError("Could not generate channel link.");
app?.showError?.("Could not generate channel link.");
return;
}
navigator.clipboard
.writeText(shareUrl)
.then(() => app.showSuccess("Channel link copied to clipboard!"))
.catch(() => app.showError("Failed to copy the link."));
.then(() => app?.showSuccess?.("Channel link copied to clipboard!"))
.catch(() => app?.showError?.("Failed to copy the link."));
});
shareBtn.dataset.initialized = "true";
@@ -136,7 +142,8 @@ function setupChannelMoreMenu() {
if (!container) {
return;
}
app.attachMoreMenuHandlers(container);
const app = getApp();
app?.attachMoreMenuHandlers?.(container);
}
async function updateChannelMenuState() {
@@ -145,6 +152,8 @@ async function updateChannelMenuState() {
return;
}
const app = getApp();
try {
await accessControl.ensureReady();
} catch (error) {
@@ -152,7 +161,7 @@ async function updateChannelMenuState() {
}
const canBlacklist =
typeof app.canCurrentUserManageBlacklist === "function"
typeof app?.canCurrentUserManageBlacklist === "function"
? app.canCurrentUserManageBlacklist()
: false;
@@ -244,12 +253,14 @@ export async function initChannelProfileView() {
currentChannelHex = hexPub;
currentChannelNpub = npub;
const app = getApp();
setupChannelShareButton();
setupChannelMoreMenu();
await updateChannelMenuState();
// 3) If user is logged in, load subscriptions and show sub/unsub button
if (app.pubkey) {
if (app?.pubkey) {
await subscriptions.loadSubscriptions(app.pubkey);
renderSubscribeButton(hexPub);
} else {
@@ -295,6 +306,8 @@ function renderSubscribeButton(channelHex) {
const container = document.getElementById("subscribeBtnArea");
if (!container) return;
const app = getApp();
container.classList.remove("hidden");
const alreadySubscribed = subscriptions.isSubscribed(channelHex);
@@ -319,7 +332,7 @@ function renderSubscribeButton(channelHex) {
const toggleBtn = document.getElementById("subscribeToggleBtn");
if (toggleBtn) {
toggleBtn.addEventListener("click", async () => {
if (!app.pubkey) {
if (!app?.pubkey) {
console.error("Not logged in => cannot subscribe/unsubscribe.");
return;
}
@@ -424,6 +437,7 @@ async function loadUserProfile(pubkey) {
* Filters out older overshadowed notes, blacklisted, etc.
*/
async function loadUserVideos(pubkey) {
const app = getApp();
try {
try {
await accessControl.ensureReady();
@@ -502,7 +516,7 @@ async function loadUserVideos(pubkey) {
// Ensure the global videos map is kept up to date so delegated handlers
// have the freshest metadata for this event.
window.app?.videosMap?.set(video.id, video);
app?.videosMap?.set(video.id, video);
// Check if user can edit
const canEdit = video.pubkey === app.pubkey;
@@ -791,17 +805,17 @@ async function loadUserVideos(pubkey) {
attachHealthBadges(container);
attachUrlHealthBadges(container, ({ badgeEl, url, eventId }) => {
const video = app.videosMap.get(eventId) || { id: eventId };
app.handleUrlHealthBadge({ video, url, badgeEl });
const video = app?.videosMap?.get(eventId) || { id: eventId };
app?.handleUrlHealthBadge?.({ video, url, badgeEl });
});
window.app.mountVideoListView();
app?.mountVideoListView?.();
// Lazy-load images
const lazyEls = container.querySelectorAll("[data-lazy]");
lazyEls.forEach((el) => app.mediaLoader.observe(el));
lazyEls.forEach((el) => app?.mediaLoader?.observe?.(el));
app.attachMoreMenuHandlers(container);
app?.attachMoreMenuHandlers?.(container);
// Gear menu toggles
const gearButtons = container.querySelectorAll("[data-settings-dropdown]");
@@ -826,7 +840,7 @@ async function loadUserVideos(pubkey) {
const dropdown = document.getElementById(`settingsDropdown-${idxAttr}`);
if (dropdown) dropdown.classList.add("hidden");
const eventId = btn.getAttribute("data-edit-event-id") || "";
app.handleEditVideo({
app?.handleEditVideo?.({
eventId,
index: Number.isNaN(idx) ? null : idx,
});
@@ -843,7 +857,7 @@ async function loadUserVideos(pubkey) {
const dropdown = document.getElementById(`settingsDropdown-${idxAttr}`);
if (dropdown) dropdown.classList.add("hidden");
const eventId = btn.getAttribute("data-revert-event-id") || "";
app.handleRevertVideo({
app?.handleRevertVideo?.({
eventId,
index: Number.isNaN(idx) ? null : idx,
});
@@ -860,7 +874,7 @@ async function loadUserVideos(pubkey) {
const dropdown = document.getElementById(`settingsDropdown-${idxAttr}`);
if (dropdown) dropdown.classList.add("hidden");
const eventId = btn.getAttribute("data-delete-all-event-id") || "";
app.handleFullDeleteVideo({
app?.handleFullDeleteVideo?.({
eventId,
index: Number.isNaN(idx) ? null : idx,
});
+2 -10
View File
@@ -12,6 +12,7 @@ import {
WATCH_HISTORY_BATCH_RESOLVE,
WATCH_HISTORY_BATCH_PAGE_SIZE,
} from "./config.js";
import { getApplication } from "./applicationContext.js";
export const WATCH_HISTORY_EMPTY_COPY =
"Your watch history is empty. Watch some videos to populate this list.";
@@ -79,16 +80,7 @@ function setTextContent(element, text) {
}
function getAppInstance() {
if (typeof window === "undefined") {
return null;
}
if (window.app) {
return window.app;
}
if (window.bitvid && window.bitvid.app) {
return window.bitvid.app;
}
return null;
return getApplication();
}
function safeLocaleDate(date) {
+2 -1
View File
@@ -2,6 +2,7 @@
import "./bufferPolyfill.js";
import { trackPageView } from "./analytics.js";
import { appReady } from "./bootstrap.js";
const INTERFACE_FADE_IN_ANIMATION = "interface-fade-in";
const VIDEO_THUMBNAIL_FADE_IN_ANIMATION = "video-thumbnail-fade-in";
@@ -367,7 +368,7 @@ function handleQueryParams() {
}
async function waitForAppInitialization() {
const maybePromise = window.appReady;
const maybePromise = appReady;
if (!maybePromise || typeof maybePromise.then !== "function") {
return;
+35 -33
View File
@@ -13,6 +13,9 @@ import {
publishEventToRelays,
assertAnyRelayAccepted,
} from "./nostrPublish.js";
import { getApplication } from "./applicationContext.js";
const getApp = () => getApplication();
let attachHealthBadgesLoader = null;
let attachHealthBadgesErrorLogged = false;
@@ -64,8 +67,9 @@ function getAbsoluteShareUrl(nevent) {
return "";
}
if (window.app?.buildShareUrlFromNevent) {
const candidate = window.app.buildShareUrlFromNevent(nevent);
const app = getApp();
if (app?.buildShareUrlFromNevent) {
const candidate = app.buildShareUrlFromNevent(nevent);
if (candidate) {
return candidate;
}
@@ -364,9 +368,11 @@ class SubscriptionsManager {
const container = document.getElementById(containerId);
if (!container) return;
const app = getApp();
const safeVideos = Array.isArray(videos) ? videos : [];
const dedupedVideos =
window.app?.dedupeVideosByRoot?.(safeVideos) ??
app?.dedupeVideosByRoot?.(safeVideos) ??
this.dedupeToNewestByRoot(safeVideos);
const filteredVideos = dedupedVideos.filter((video) => {
@@ -374,10 +380,7 @@ class SubscriptionsManager {
return false;
}
if (
window.app?.isAuthorBlocked &&
window.app.isAuthorBlocked(video.pubkey)
) {
if (app?.isAuthorBlocked && app.isAuthorBlocked(video.pubkey)) {
return false;
}
@@ -408,26 +411,26 @@ class SubscriptionsManager {
// Keep the global videos map up to date so delegated playback handlers
// can reuse the already fetched metadata for this event.
window.app?.videosMap?.set(video.id, video);
app?.videosMap?.set(video.id, video);
localAuthorSet.add(video.pubkey);
const nevent = window.NostrTools.nip19.neventEncode({ id: video.id });
const shareUrl = getAbsoluteShareUrl(nevent);
const canEdit = window.app?.pubkey === video.pubkey;
const canEdit = app?.pubkey === video.pubkey;
const highlightClass =
video.isPrivate && canEdit
? "border-2 border-yellow-500"
: "border-none";
const timeAgo = window.app?.formatTimeAgo
? window.app.formatTimeAgo(video.created_at)
const timeAgo = app?.formatTimeAgo
? app.formatTimeAgo(video.created_at)
: new Date(video.created_at * 1000).toLocaleString();
let hasOlder = false;
if (canEdit && video.videoRootId && window.app?.hasOlderVersion) {
hasOlder = window.app.hasOlderVersion(video, fullAllEventsArray);
if (canEdit && video.videoRootId && app?.hasOlderVersion) {
hasOlder = app.hasOlderVersion(video, fullAllEventsArray);
}
const revertButton = hasOlder
@@ -524,8 +527,8 @@ class SubscriptionsManager {
</div>
`;
const safeTitle = window.app?.escapeHTML(video.title) || "Untitled";
const safeThumb = window.app?.escapeHTML(video.thumbnail) || "";
const safeTitle = app?.escapeHTML?.(video.title) || "Untitled";
const safeThumb = app?.escapeHTML?.(video.thumbnail) || "";
const playbackUrl =
typeof video.url === "string" ? video.url : "";
const trimmedUrl = playbackUrl ? playbackUrl.trim() : "";
@@ -537,14 +540,14 @@ class SubscriptionsManager {
const playbackMagnet = magnetCandidate;
const magnetProvided = magnetCandidate.length > 0;
const magnetSupported =
window.app?.isMagnetUriSupported?.(magnetCandidate) ?? false;
app?.isMagnetUriSupported?.(magnetCandidate) ?? false;
const urlBadgeHtml = trimmedUrl
? window.app?.getUrlHealthPlaceholderMarkup?.({ includeMargin: false }) ??
""
? app?.getUrlHealthPlaceholderMarkup?.({ includeMargin: false }) ??
""
: "";
const torrentHealthBadgeHtml =
magnetProvided && magnetSupported
? window.app?.getTorrentHealthBadgeMarkup?.({
? app?.getTorrentHealthBadgeMarkup?.({
includeMargin: false,
}) ?? ""
: "";
@@ -696,21 +699,20 @@ class SubscriptionsManager {
container.appendChild(fragment);
scheduleAttachHealthBadges(container);
attachUrlHealthBadges(container, ({ badgeEl, url, eventId }) => {
if (!window.app?.handleUrlHealthBadge) {
if (!app?.handleUrlHealthBadge) {
return;
}
const video =
window.app.videosMap?.get?.(eventId) || { id: eventId };
window.app.handleUrlHealthBadge({ video, url, badgeEl });
const video = app.videosMap?.get?.(eventId) || { id: eventId };
app.handleUrlHealthBadge({ video, url, badgeEl });
});
window.app?.mountVideoListView?.();
app?.mountVideoListView?.();
// Lazy-load
const lazyEls = container.querySelectorAll("[data-lazy]");
lazyEls.forEach((el) => window.app?.mediaLoader.observe(el));
lazyEls.forEach((el) => app?.mediaLoader?.observe?.(el));
window.app?.attachMoreMenuHandlers?.(container);
app?.attachMoreMenuHandlers?.(container);
// Gear menus
const gearButtons = container.querySelectorAll("[data-settings-dropdown]");
@@ -733,7 +735,7 @@ class SubscriptionsManager {
const dropdown = document.getElementById(`settingsDropdown-${idxAttr}`);
if (dropdown) dropdown.classList.add("hidden");
const eventId = btn.getAttribute("data-edit-event-id") || "";
window.app?.handleEditVideo({
app?.handleEditVideo?.({
eventId,
index: Number.isNaN(idx) ? null : idx,
});
@@ -750,7 +752,7 @@ class SubscriptionsManager {
const dropdown = document.getElementById(`settingsDropdown-${idxAttr}`);
if (dropdown) dropdown.classList.add("hidden");
const eventId = btn.getAttribute("data-revert-event-id") || "";
window.app?.handleRevertVideo({
app?.handleRevertVideo?.({
eventId,
index: Number.isNaN(idx) ? null : idx,
});
@@ -769,7 +771,7 @@ class SubscriptionsManager {
const dd = document.getElementById(`settingsDropdown-${idxAttr}`);
if (dd) dd.classList.add("hidden");
const eventId = btn.getAttribute("data-delete-all-event-id") || "";
window.app?.handleFullDeleteVideo({
app?.handleFullDeleteVideo?.({
eventId,
index: Number.isNaN(idx) ? null : idx,
});
@@ -789,8 +791,8 @@ class SubscriptionsManager {
localAuthorSet.add(nameEl.getAttribute("data-pubkey"));
});
if (window.app?.batchFetchProfiles && localAuthorSet.size > 0) {
window.app.batchFetchProfiles(localAuthorSet);
if (app?.batchFetchProfiles && localAuthorSet.size > 0) {
app.batchFetchProfiles(localAuthorSet);
}
// Make author name/pic clickable => open channel
@@ -800,7 +802,7 @@ class SubscriptionsManager {
ev.preventDefault();
ev.stopPropagation();
const pubkey = pic.getAttribute("data-pubkey");
window.app?.goToProfile(pubkey);
app?.goToProfile?.(pubkey);
});
});
@@ -810,7 +812,7 @@ class SubscriptionsManager {
ev.preventDefault();
ev.stopPropagation();
const pubkey = nameEl.getAttribute("data-pubkey");
window.app?.goToProfile(pubkey);
app?.goToProfile?.(pubkey);
});
});
}
+13
View File
@@ -0,0 +1,13 @@
// js/utils/fakeDecrypt.js
/**
* Simple "decryption" placeholder for private videos.
*/
export function fakeDecrypt(str) {
if (typeof str !== "string") {
return "";
}
return str.split("").reverse().join("");
}
export default fakeDecrypt;
+59
View File
@@ -0,0 +1,59 @@
// js/utils/magnetValidators.js
import { safeDecodeMagnet } from "../magnetUtils.js";
/**
* Basic validation for BitTorrent magnet URIs.
*
* Returns `true` only when the value looks like a magnet link that WebTorrent
* understands (`magnet:` scheme with at least one `xt=urn:btih:<info-hash>`
* entry, where `<info-hash>` is either a 40-character hex digest or a
* 32-character base32 digest). Magnets that only contain BitTorrent v2 hashes
* (e.g. `btmh`) are treated as unsupported.
*/
export function isValidMagnetUri(magnet) {
const trimmed = typeof magnet === "string" ? magnet.trim() : "";
if (!trimmed) {
return false;
}
const decoded = safeDecodeMagnet(trimmed);
const candidate = decoded || trimmed;
if (/^[0-9a-f]{40}$/i.test(candidate)) {
return true;
}
try {
const parsed = new URL(candidate);
if (parsed.protocol.toLowerCase() !== "magnet:") {
return false;
}
const xtValues = parsed.searchParams.getAll("xt");
if (!xtValues.length) {
return false;
}
const hexPattern = /^[0-9a-f]{40}$/i;
const base32Pattern = /^[A-Z2-7]{32}$/;
return xtValues.some((value) => {
if (typeof value !== "string") return false;
const match = value.trim().match(/^urn:btih:([a-z0-9]+)$/i);
if (!match) return false;
const infoHash = match[1];
if (hexPattern.test(infoHash)) {
return true;
}
const upperHash = infoHash.toUpperCase();
return infoHash.length === 32 && base32Pattern.test(upperHash);
});
} catch (err) {
return false;
}
}
export default isValidMagnetUri;
+82
View File
@@ -0,0 +1,82 @@
// js/utils/mediaLoader.js
/**
* Simple IntersectionObserver-based lazy loader for images (or videos).
*
* Usage:
* const mediaLoader = new MediaLoader();
* mediaLoader.observe(imgElement);
*
* This will load the real image source from `imgElement.dataset.lazy`
* once the image enters the viewport.
*/
export class MediaLoader {
constructor(rootMargin = "50px") {
this.observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) {
continue;
}
const el = entry.target;
const lazySrc =
typeof el.dataset.lazy === "string" ? el.dataset.lazy.trim() : "";
if (!lazySrc) {
continue;
}
const fallbackSrc =
(typeof el.dataset.fallbackSrc === "string"
? el.dataset.fallbackSrc.trim()
: "") ||
el.getAttribute("data-fallback-src") ||
"";
if (el.tagName === "IMG" || el.tagName === "IMAGE") {
el.src = lazySrc;
if (fallbackSrc) {
el.onerror = () => {
el.src = fallbackSrc;
};
}
} else if (el.tagName === "VIDEO") {
el.src = lazySrc;
if (fallbackSrc) {
el.poster = fallbackSrc;
}
} else {
el.style.backgroundImage = `url('${lazySrc}')`;
}
el.dataset.thumbnailLoaded = "true";
this.observer.unobserve(el);
}
}, {
rootMargin,
threshold: 0.01,
});
}
observe(el) {
if (!el || typeof el !== "object") {
return;
}
if ("decoding" in HTMLImageElement.prototype) {
el.decoding = el.decoding || "async";
}
const lazySrc =
typeof el.dataset.lazy === "string" ? el.dataset.lazy.trim() : "";
if (lazySrc) {
this.observer.observe(el);
}
}
disconnect() {
this.observer.disconnect();
}
}
export default MediaLoader;
+27
View File
@@ -0,0 +1,27 @@
// js/utils/pointer.js
export function pointerArrayToKey(pointer) {
if (!Array.isArray(pointer) || pointer.length < 2) {
return "";
}
const type = pointer[0] === "a" ? "a" : pointer[0] === "e" ? "e" : "";
if (!type) {
return "";
}
const value =
typeof pointer[1] === "string" ? pointer[1].trim().toLowerCase() : "";
if (!value) {
return "";
}
const relay =
pointer.length > 2 && typeof pointer[2] === "string"
? pointer[2].trim()
: "";
return relay ? `${type}:${value}:${relay}` : `${type}:${value}`;
}
export default pointerArrayToKey;
+29
View File
@@ -0,0 +1,29 @@
// js/utils/videoDeduper.js
/**
* Given an array of video objects, return only the newest (by created_at) for
* each videoRootId. If no videoRootId is present, treat the video’s own ID as
* its root.
*/
export function dedupeToNewestByRoot(videos) {
const map = new Map();
for (const vid of videos || []) {
if (!vid) {
continue;
}
const rootId = vid.videoRootId || vid.id;
if (!rootId) {
continue;
}
const existing = map.get(rootId);
if (!existing || vid.created_at > existing.created_at) {
map.set(rootId, vid);
}
}
return Array.from(map.values());
}
export default dedupeToNewestByRoot;
+15 -10
View File
@@ -1,6 +1,7 @@
// js/viewManager.js
import { initChannelProfileView } from "./channelProfile.js";
import { subscriptions } from "./subscriptions.js";
import { getApplication } from "./applicationContext.js";
const TRACKING_SCRIPT_PATTERN = /(?:^|\/)tracking\.js(?:$|\?)/;
@@ -9,8 +10,9 @@ const TRACKING_SCRIPT_PATTERN = /(?:^|\/)tracking\.js(?:$|\?)/;
*/
export async function loadView(viewUrl) {
try {
if (window.app && typeof window.app.prepareForViewLoad === "function") {
window.app.prepareForViewLoad();
const app = getApplication();
if (app && typeof app.prepareForViewLoad === "function") {
app.prepareForViewLoad();
}
const res = await fetch(viewUrl);
@@ -52,15 +54,17 @@ export async function loadView(viewUrl) {
*/
export const viewInitRegistry = {
"most-recent-videos": () => {
if (window.app && window.app.loadVideos) {
if (typeof window.app.mountVideoListView === "function") {
window.app.mountVideoListView();
const app = getApplication();
if (app && typeof app.loadVideos === "function") {
if (typeof app.mountVideoListView === "function") {
app.mountVideoListView();
}
window.app.loadVideos();
app.loadVideos();
}
// Force profile updates after the new view is in place.
if (window.app && window.app.forceRefreshAllProfiles) {
window.app.forceRefreshAllProfiles();
const refreshApp = getApplication();
if (refreshApp && typeof refreshApp.forceRefreshAllProfiles === "function") {
refreshApp.forceRefreshAllProfiles();
}
},
explore: () => {
@@ -85,7 +89,8 @@ export const viewInitRegistry = {
subscriptions: async () => {
console.log("Subscriptions view loaded.");
if (!window.app.pubkey) {
const app = getApplication();
if (!app?.pubkey) {
const container = document.getElementById("subscriptionsVideoList");
if (container) {
container.innerHTML =
@@ -96,7 +101,7 @@ export const viewInitRegistry = {
// If user is logged in, let the SubscriptionsManager do everything:
await subscriptions.showSubscriptionVideos(
window.app.pubkey,
app.pubkey,
"subscriptionsVideoList"
);
},
+2 -1
View File
@@ -15,6 +15,7 @@ import {
getWatchHistoryV2Enabled,
} from "./constants.js";
import { isDevMode } from "./config.js";
import { getApplication } from "./applicationContext.js";
const SESSION_STORAGE_KEY = "bitvid:watch-history:session:v1";
const SESSION_STORAGE_VERSION = 1;
@@ -60,7 +61,7 @@ function getLoggedInActorKey() {
if (typeof window !== "undefined") {
const appCandidate =
(window.bitvid && window.bitvid.app) || window.app || null;
getApplication() || null;
if (appCandidate && typeof appCandidate === "object") {
if (typeof appCandidate.normalizeHexPubkey === "function") {
try {
+7 -8
View File
@@ -14,6 +14,9 @@ const {
chunkWatchHistoryPayloadItems,
} = await import("../js/nostr.js");
const { watchHistoryService } = await import("../js/watchHistoryService.js");
const { getApplication, setApplication } = await import(
"../js/applicationContext.js"
);
if (typeof globalThis.window === "undefined") {
globalThis.window = {};
@@ -1154,7 +1157,7 @@ async function testWatchHistoryAppLoginFallback() {
const actor = "f".repeat(64);
const originalPub = nostrClient.pubkey;
const originalSession = nostrClient.sessionActor;
const originalApp = typeof window.app === "undefined" ? undefined : window.app;
const originalApp = getApplication();
try {
setWatchHistoryV2Enabled(false);
@@ -1162,7 +1165,7 @@ async function testWatchHistoryAppLoginFallback() {
watchHistoryService.resetProgress();
nostrClient.pubkey = "";
nostrClient.sessionActor = null;
window.app = {
setApplication({
pubkey: actor,
normalizeHexPubkey(value) {
if (typeof value === "string" && value.trim()) {
@@ -1170,7 +1173,7 @@ async function testWatchHistoryAppLoginFallback() {
}
return null;
},
};
});
const enabled =
typeof watchHistoryService.isEnabled === "function"
@@ -1186,11 +1189,7 @@ async function testWatchHistoryAppLoginFallback() {
watchHistoryService.resetProgress();
nostrClient.pubkey = originalPub;
nostrClient.sessionActor = originalSession;
if (typeof originalApp === "undefined") {
delete window.app;
} else {
window.app = originalApp;
}
setApplication(originalApp || null);
}
}