From 749818fa13bcfcd24a2921233653ae18cdefe567 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:59:01 -0400 Subject: [PATCH 01/10] Refine profile modal teardown handling --- js/app.js | 1 - js/ui/profileModalController.js | 129 ++++++++++++++++++++++++-------- 2 files changed, 97 insertions(+), 33 deletions(-) diff --git a/js/app.js b/js/app.js index 8788f662..72841bff 100644 --- a/js/app.js +++ b/js/app.js @@ -294,7 +294,6 @@ class Application { this.isWalletPaneBusy = false; this.profileModalCachedSelection = null; this.profileModalController = null; - this.boundProfileHistoryVisibility = null; this.profileSwitcherSelectionPubkey = null; this.currentUserNpub = null; diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index b4cc27f5..1b030def 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -675,6 +675,7 @@ export class ProfileModalController { this.adminBlacklistInput = null; this.profileHistoryRenderer = null; + this.profileHistoryRendererConfig = null; this.boundProfileHistoryVisibility = null; this.boundKeydown = null; this.boundFocusIn = null; @@ -840,36 +841,70 @@ export class ProfileModalController { this.adminAddBlacklistButton = this.addBlacklistButton; this.adminBlacklistInput = this.blacklistInput; - if (!this.profileHistoryRenderer && this.createWatchHistoryRenderer) { - this.profileHistoryRenderer = this.createWatchHistoryRenderer({ - viewSelector: "#profilePaneHistory", - gridSelector: "#profileHistoryGrid", - loadingSelector: "#profileHistoryLoading", - statusSelector: "#profileHistoryStatus", - emptySelector: "#profileHistoryEmpty", - sentinelSelector: "#profileHistorySentinel", - scrollContainerSelector: "#profileHistoryScroll", - errorBannerSelector: "#profileHistoryError", - clearButtonSelector: "#profileHistoryClear", - republishButtonSelector: "#profileHistoryRepublish", - featureBannerSelector: "#profileHistoryFeatureBanner", - toastRegionSelector: "#profileHistoryToastRegion", - sessionWarningSelector: "#profileHistorySessionWarning", - metadataToggleSelector: "#profileHistoryMetadataToggle", - metadataThumbSelector: "#profileHistoryMetadataThumb", - metadataLabelSelector: "#profileHistoryMetadataLabel", - metadataDescriptionSelector: "#profileHistoryMetadataDescription", - emptyCopy: "You haven’t watched any videos yet.", - remove: (payload) => - this.callbacks.onHistoryReady({ - ...(typeof payload === "object" && payload ? payload : {}), - controller: this, - renderer: this.profileHistoryRenderer, - }), - }); + if (this.createWatchHistoryRenderer) { + this.ensureProfileHistoryRenderer(); } } + ensureProfileHistoryRenderer() { + if (this.profileHistoryRenderer) { + return this.profileHistoryRenderer; + } + + if (!this.createWatchHistoryRenderer) { + return null; + } + + const config = this.getProfileHistoryRendererConfig(); + + try { + this.profileHistoryRenderer = this.createWatchHistoryRenderer(config); + } catch (error) { + console.error( + "[profileModal] Failed to create watch history renderer:", + error, + ); + this.profileHistoryRenderer = null; + } + + return this.profileHistoryRenderer; + } + + getProfileHistoryRendererConfig() { + if (this.profileHistoryRendererConfig) { + return this.profileHistoryRendererConfig; + } + + this.profileHistoryRendererConfig = { + viewSelector: "#profilePaneHistory", + gridSelector: "#profileHistoryGrid", + loadingSelector: "#profileHistoryLoading", + statusSelector: "#profileHistoryStatus", + emptySelector: "#profileHistoryEmpty", + sentinelSelector: "#profileHistorySentinel", + scrollContainerSelector: "#profileHistoryScroll", + errorBannerSelector: "#profileHistoryError", + clearButtonSelector: "#profileHistoryClear", + republishButtonSelector: "#profileHistoryRepublish", + featureBannerSelector: "#profileHistoryFeatureBanner", + toastRegionSelector: "#profileHistoryToastRegion", + sessionWarningSelector: "#profileHistorySessionWarning", + metadataToggleSelector: "#profileHistoryMetadataToggle", + metadataThumbSelector: "#profileHistoryMetadataThumb", + metadataLabelSelector: "#profileHistoryMetadataLabel", + metadataDescriptionSelector: "#profileHistoryMetadataDescription", + emptyCopy: "You haven’t watched any videos yet.", + remove: (payload) => + this.callbacks.onHistoryReady({ + ...(typeof payload === "object" && payload ? payload : {}), + controller: this, + renderer: this.profileHistoryRenderer, + }), + }; + + return this.profileHistoryRendererConfig; + } + registerEventListeners() { if (this.closeButton instanceof HTMLElement) { this.closeButton.addEventListener("click", () => { @@ -1947,7 +1982,8 @@ export class ProfileModalController { } async populateProfileWatchHistory() { - if (!this.profileHistoryRenderer) { + const renderer = this.ensureProfileHistoryRenderer(); + if (!renderer) { return; } @@ -1960,8 +1996,8 @@ export class ProfileModalController { } try { - await this.profileHistoryRenderer.ensureInitialLoad({ actor: primaryActor }); - await this.profileHistoryRenderer.refresh({ + await renderer.ensureInitialLoad({ actor: primaryActor }); + await renderer.refresh({ actor: primaryActor, force: true, }); @@ -1984,9 +2020,9 @@ export class ProfileModalController { } if (document.visibilityState === "hidden") { - this.profileHistoryRenderer.pause(); + renderer.pause(); } else { - this.profileHistoryRenderer.resume(); + renderer.resume(); } } catch (error) { console.error( @@ -3701,6 +3737,35 @@ export class ProfileModalController { document.removeEventListener("focusin", this.boundFocusIn); } + if ( + this.boundProfileHistoryVisibility && + typeof document !== "undefined" && + typeof document.removeEventListener === "function" + ) { + document.removeEventListener( + "visibilitychange", + this.boundProfileHistoryVisibility, + ); + this.boundProfileHistoryVisibility = null; + } + + if (this.profileHistoryRenderer) { + try { + if (typeof this.profileHistoryRenderer.destroy === "function") { + this.profileHistoryRenderer.destroy(); + } + } catch (error) { + console.warn( + "[profileModal] Failed to reset watch history renderer on close:", + error, + ); + } + this.profileHistoryRenderer = null; + } + + this.setActivePane(null); + this.setWalletPaneBusy(false); + const previous = this.previouslyFocusedElement; this.previouslyFocusedElement = null; From 814bb65ffe94a9cbf60ebb8e27fc068b9a4c1b8f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:03:41 -0400 Subject: [PATCH 02/10] Fix profile modal teardown state reset --- js/app.js | 5 +++++ js/ui/profileModalController.js | 1 + 2 files changed, 6 insertions(+) diff --git a/js/app.js b/js/app.js index 72841bff..9382085d 100644 --- a/js/app.js +++ b/js/app.js @@ -1284,6 +1284,11 @@ class Application { return pane; }, setActivePane: (pane) => { + if (pane === null) { + this.activeProfilePane = null; + return this.activeProfilePane; + } + const normalized = typeof pane === "string" && pane.trim() ? pane.trim().toLowerCase() diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index 1b030def..541fb23d 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -3761,6 +3761,7 @@ export class ProfileModalController { ); } this.profileHistoryRenderer = null; + this.profileHistoryRendererConfig = null; } this.setActivePane(null); From 7070eea35ec1b6ee9eaa78cbd8464597133bfb93 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:12:48 -0400 Subject: [PATCH 03/10] Ensure channel grid cards trigger playback --- js/channelProfile.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/js/channelProfile.js b/js/channelProfile.js index d20db4cf..396b6a50 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -1824,6 +1824,11 @@ async function loadUserVideos(pubkey) { } else if (cardEl.dataset.ownerPubkey) { delete cardEl.dataset.ownerPubkey; } + if (typeof video.id === "string" && video.id) { + cardEl.dataset.videoId = video.id; + } else if (cardEl.dataset.videoId) { + delete cardEl.dataset.videoId; + } const rawMagnet = typeof video.magnet === "string" ? video.magnet : ""; @@ -1933,6 +1938,14 @@ async function loadUserVideos(pubkey) { delete cardEl.dataset.magnet; } + if (trimmedUrl) { + cardEl.dataset.playUrl = encodeURIComponent(trimmedUrl); + } else if (cardEl.dataset.playUrl) { + delete cardEl.dataset.playUrl; + } + + cardEl.dataset.playMagnet = playbackMagnet || ""; + // Leave the data-play-* attributes empty in the template markup so the raw // URL/magnet strings can be assigned after parsing without HTML entity // escaping, keeping this renderer consistent with app.js. The stored URL is From daf39ce36195cb6073461a5ba503385bf58b8add Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:21:28 -0400 Subject: [PATCH 04/10] Improve channel grid loading responsiveness --- js/channelProfile.js | 61 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index 396b6a50..3e040a05 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -118,6 +118,7 @@ let cachedZapSendBtn = null; let pendingZapRetry = null; let zapInFlight = false; let zapControlsOpen = false; +let currentVideoLoadToken = 0; function getChannelZapButton() { if (cachedZapButton && !document.body.contains(cachedZapButton)) { @@ -1584,13 +1585,32 @@ async function loadUserProfile(pubkey) { */ async function loadUserVideos(pubkey) { const app = getApp(); - try { - try { - await accessControl.ensureReady(); - } catch (error) { - console.warn("Failed to ensure admin lists were loaded before channel fetch:", error); - } + const container = document.getElementById("channelVideoList"); + const loadToken = ++currentVideoLoadToken; + const hadExistingContent = + !!container && container.querySelector("[data-video-id]"); + if (container) { + container.dataset.loading = "true"; + if (!hadExistingContent) { + container.innerHTML = ` +
No videos to display.
`; return; } + container.dataset.hasChannelVideos = "true"; const fragment = document.createDocumentFragment(); const allKnownEventsArray = Array.from(nostrClient.allEvents.values()); @@ -2134,7 +2162,18 @@ async function loadUserVideos(pubkey) { }); }); } catch (err) { + if (loadToken === currentVideoLoadToken && container) { + container.dataset.hasChannelVideos = "false"; + container.innerHTML = ` +Failed to load videos. Please try again.
+ `; + } console.error("Error loading user videos:", err); + } finally { + await ensureAccessPromise; + if (loadToken === currentVideoLoadToken && container) { + delete container.dataset.loading; + } } } From 9531fb21be5a383455aaa3192b15ea42e8448ba8 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:30:07 -0400 Subject: [PATCH 05/10] Ensure channel grid passes playback sources to modal --- js/app.js | 68 ++++++++++++++++++++++++++++++++++++++------ js/channelProfile.js | 5 +++- js/historyView.js | 18 +++++++++--- js/subscriptions.js | 7 ++++- 4 files changed, 83 insertions(+), 15 deletions(-) diff --git a/js/app.js b/js/app.js index 9382085d..60ce6339 100644 --- a/js/app.js +++ b/js/app.js @@ -638,7 +638,9 @@ class Application { this.videoListViewPlaybackHandler = ({ videoId, url, magnet }) => { if (videoId) { - Promise.resolve(this.playVideoByEventId(videoId)).catch((error) => { + Promise.resolve( + this.playVideoByEventId(videoId, { url, magnet }) + ).catch((error) => { console.error("[VideoListView] Failed to play by event id:", error); }); return; @@ -7174,12 +7176,30 @@ class Application { } - async playVideoByEventId(eventId) { + async playVideoByEventId(eventId, playbackHint = {}) { if (!eventId) { this.showError("No video identifier provided."); return; } + const fallbackUrl = + typeof playbackHint?.url === "string" ? playbackHint.url.trim() : ""; + const fallbackTitle = + typeof playbackHint?.title === "string" ? playbackHint.title : ""; + const fallbackDescription = + typeof playbackHint?.description === "string" + ? playbackHint.description + : ""; + const fallbackMagnetRaw = + typeof playbackHint?.magnet === "string" + ? playbackHint.magnet.trim() + : ""; + let fallbackMagnetCandidate = ""; + if (fallbackMagnetRaw) { + const decoded = safeDecodeMagnet(fallbackMagnetRaw); + fallbackMagnetCandidate = decoded || fallbackMagnetRaw; + } + this.currentVideoPointer = null; this.currentVideoPointerKey = null; @@ -7193,6 +7213,14 @@ class Application { video = await this.getOldEventById(eventId); } if (!video) { + if (fallbackUrl || fallbackMagnetCandidate) { + return this.playVideoWithoutEvent({ + url: fallbackUrl, + magnet: fallbackMagnetCandidate, + title: fallbackTitle || "Untitled", + description: fallbackDescription || "", + }); + } this.showError("Video not found or has been removed."); return; } @@ -7223,15 +7251,35 @@ class Application { video.alreadyDecrypted = true; } - const trimmedUrl = typeof video.url === "string" ? video.url.trim() : ""; + let trimmedUrl = + typeof video.url === "string" ? video.url.trim() : ""; + if (!trimmedUrl && fallbackUrl) { + trimmedUrl = fallbackUrl; + } const rawMagnet = typeof video.magnet === "string" ? video.magnet.trim() : ""; - const legacyInfoHash = + let legacyInfoHash = typeof video.infoHash === "string" ? video.infoHash.trim().toLowerCase() : ""; - const magnetCandidate = rawMagnet || legacyInfoHash; - const decodedMagnetCandidate = safeDecodeMagnet(magnetCandidate); - const usableMagnetCandidate = decodedMagnetCandidate || magnetCandidate; - const magnetSupported = isValidMagnetUri(usableMagnetCandidate); + const fallbackMagnetForCandidate = fallbackMagnetCandidate || ""; + if (!legacyInfoHash && fallbackMagnetForCandidate) { + const match = fallbackMagnetForCandidate.match(/xt=urn:btih:([0-9a-z]+)/i); + if (match && match[1]) { + legacyInfoHash = match[1].toLowerCase(); + } + } + + let magnetCandidate = rawMagnet || legacyInfoHash || ""; + let decodedMagnetCandidate = safeDecodeMagnet(magnetCandidate); + let usableMagnetCandidate = decodedMagnetCandidate || magnetCandidate; + let magnetSupported = isValidMagnetUri(usableMagnetCandidate); + + if (!magnetSupported && fallbackMagnetForCandidate) { + magnetCandidate = fallbackMagnetForCandidate; + decodedMagnetCandidate = safeDecodeMagnet(magnetCandidate); + usableMagnetCandidate = decodedMagnetCandidate || magnetCandidate; + magnetSupported = isValidMagnetUri(usableMagnetCandidate); + } + const sanitizedMagnet = magnetSupported ? usableMagnetCandidate : ""; trackVideoView({ @@ -7246,7 +7294,8 @@ class Application { ...video, url: trimmedUrl, magnet: sanitizedMagnet, - originalMagnet: magnetCandidate, + originalMagnet: + magnetCandidate || fallbackMagnetForCandidate || legacyInfoHash || "", torrentSupported: magnetSupported, legacyInfoHash: video.legacyInfoHash || legacyInfoHash, lightningAddress: null, @@ -7371,6 +7420,7 @@ class Application { sanitizedMagnet || decodedMagnetCandidate || magnetCandidate || + fallbackMagnetForCandidate || legacyInfoHash || ""; diff --git a/js/channelProfile.js b/js/channelProfile.js index 3e040a05..2bbcd26c 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -2079,7 +2079,10 @@ async function loadUserVideos(pubkey) { if (videoId && typeof app?.playVideoByEventId === "function") { try { - const result = await app.playVideoByEventId(videoId); + const result = await app.playVideoByEventId(videoId, { + url, + magnet, + }); if (!result || result.error) { playWithUrlAndMagnet(); } diff --git a/js/historyView.js b/js/historyView.js index 4ea51288..003c83a4 100644 --- a/js/historyView.js +++ b/js/historyView.js @@ -1290,16 +1290,26 @@ export function createWatchHistoryRenderer(config = {}) { case "play": { event.preventDefault(); const videoId = trigger.dataset.videoId || pointerKeyAttr; + const rawUrlAttr = trigger.dataset.playUrl || ""; + const magnetAttr = trigger.dataset.playMagnet || ""; + let url = ""; + if (rawUrlAttr) { + try { + url = decodeURIComponent(rawUrlAttr); + } catch (error) { + url = rawUrlAttr; + } + } + const app = getAppInstance(); if (videoId && app?.playVideoByEventId) { - app.playVideoByEventId(videoId); + app.playVideoByEventId(videoId, { url, magnet: magnetAttr }); return; } - const urlAttr = trigger.dataset.playUrl || ""; - const magnetAttr = trigger.dataset.playMagnet || ""; + if (app?.playVideoWithFallback) { app.playVideoWithFallback({ - url: urlAttr ? decodeURIComponent(urlAttr) : "", + url, magnet: magnetAttr, }); } diff --git a/js/subscriptions.js b/js/subscriptions.js index d8e191c4..63ba063f 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -581,7 +581,12 @@ class SubscriptionsManager { return; } if (detail.videoId) { - Promise.resolve(app?.playVideoByEventId?.(detail.videoId)).catch( + Promise.resolve( + app?.playVideoByEventId?.(detail.videoId, { + url: detail.url, + magnet: detail.magnet, + }) + ).catch( (error) => { console.error( "[SubscriptionsManager] Failed to play by event id:", From ea81e3bf32e9d456bbb384c21051ec3ccd7b252a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:36:35 -0400 Subject: [PATCH 06/10] Return playback session result --- js/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/app.js b/js/app.js index 60ce6339..8c11925f 100644 --- a/js/app.js +++ b/js/app.js @@ -7424,7 +7424,7 @@ class Application { legacyInfoHash || ""; - await this.playVideoWithFallback({ + return this.playVideoWithFallback({ url: trimmedUrl, magnet: magnetInput, }); @@ -7507,7 +7507,7 @@ class Application { const cleaned = `${urlObj.pathname}${urlObj.search}${urlObj.hash}`; window.history.replaceState({}, "", cleaned); - await this.playVideoWithFallback({ + return this.playVideoWithFallback({ url: sanitizedUrl, magnet: usableMagnet, }); From 7995f92a3855fcb90ca117b06db2c09f8e7d5c16 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:46:46 -0400 Subject: [PATCH 07/10] Prevent duplicate modal playback sessions --- js/app.js | 76 ++++++++++++++++++++++++---------- js/services/playbackService.js | 37 +++++++++++++++++ 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/js/app.js b/js/app.js index 8c11925f..10e44621 100644 --- a/js/app.js +++ b/js/app.js @@ -238,29 +238,30 @@ class Application { deriveTorrentPlaybackConfig, isValidMagnetUri, urlFirstEnabled: URL_FIRST_ENABLED, - analyticsCallbacks: { - "session-start": (detail) => { - const urlProvided = detail?.urlProvided ? "true" : "false"; - const magnetProvided = detail?.magnetProvided ? "true" : "false"; - const magnetUsable = detail?.magnetUsable ? "true" : "false"; - this.log( - `[playVideoWithFallback] Session start urlProvided=${urlProvided} magnetProvided=${magnetProvided} magnetUsable=${magnetUsable}` - ); - }, - fallback: (detail) => { - if (detail?.reason) { + analyticsCallbacks: { + "session-start": (detail) => { + const urlProvided = detail?.urlProvided ? "true" : "false"; + const magnetProvided = detail?.magnetProvided ? "true" : "false"; + const magnetUsable = detail?.magnetUsable ? "true" : "false"; this.log( - `[playVideoWithFallback] Falling back to WebTorrent (${detail.reason}).` + `[playVideoWithFallback] Session start urlProvided=${urlProvided} magnetProvided=${magnetProvided} magnetUsable=${magnetUsable}` ); - } + }, + fallback: (detail) => { + if (detail?.reason) { + this.log( + `[playVideoWithFallback] Falling back to WebTorrent (${detail.reason}).` + ); + } + }, + error: (detail) => { + if (detail?.error) { + this.log("[playVideoWithFallback] Playback error observed", detail.error); + } + }, }, - error: (detail) => { - if (detail?.error) { - this.log("[playVideoWithFallback] Playback error observed", detail.error); - } - }, - }, }); + this.activePlaybackResultPromise = null; this.activePlaybackSession = null; this.authService = @@ -7009,11 +7010,32 @@ class Application { * and falls back to WebTorrent when needed. */ async playVideoWithFallback({ url = "", magnet = "" } = {}) { - await this.waitForCleanup(); - this.cancelPendingViewLogging(); - const sanitizedUrl = typeof url === "string" ? url.trim() : ""; const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; + const requestSignature = JSON.stringify({ + url: sanitizedUrl, + magnet: trimmedMagnet, + }); + + if ( + this.activePlaybackSession && + typeof this.activePlaybackSession.matchesRequestSignature === "function" && + this.activePlaybackSession.matchesRequestSignature(requestSignature) + ) { + this.log( + "[playVideoWithFallback] Duplicate playback request detected; reusing active session." + ); + if (this.activePlaybackResultPromise) { + return this.activePlaybackResultPromise; + } + if (typeof this.activePlaybackSession.getResult === "function") { + return this.activePlaybackSession.getResult(); + } + return { source: null }; + } + + await this.waitForCleanup(); + this.cancelPendingViewLogging(); let modalVideoEl = this.modalVideo; if (!modalVideoEl && this.videoModal) { @@ -7062,11 +7084,15 @@ class Application { if (refreshedModal) { this.modalVideo = refreshedModal; modalVideoEl = this.modalVideo; + this.applyModalLoadingPoster(); + } else { + this.applyModalLoadingPoster(); } const session = this.playbackService.createSession({ url: sanitizedUrl, magnet: trimmedMagnet, + requestSignature, videoElement: this.modalVideo, waitForCleanup: () => this.waitForCleanup(), cancelPendingViewLogging: () => this.cancelPendingViewLogging(), @@ -7082,6 +7108,7 @@ class Application { }); this.activePlaybackSession = session; + this.activePlaybackResultPromise = null; this.resetTorrentStats(); this.playSource = null; @@ -7150,6 +7177,7 @@ class Application { subscribe("finished", () => { if (this.activePlaybackSession === session) { this.activePlaybackSession = null; + this.activePlaybackResultPromise = null; } while (unsubscribers.length) { const off = unsubscribers.pop(); @@ -7166,7 +7194,9 @@ class Application { } }); - const result = await session.start(); + const startPromise = session.start(); + this.activePlaybackResultPromise = startPromise; + const result = await startPromise; if (!result || result.error) { return result; diff --git a/js/services/playbackService.js b/js/services/playbackService.js index 4551f5bf..b00cfa53 100644 --- a/js/services/playbackService.js +++ b/js/services/playbackService.js @@ -234,11 +234,20 @@ class PlaybackSession extends SimpleEventEmitter { this.options = options; this.watchdogCleanup = null; this.result = { source: null }; + this.finished = false; + this.startPromise = null; this.sanitizedUrl = typeof options.url === "string" ? options.url.trim() : ""; this.trimmedMagnet = typeof options.magnet === "string" ? options.magnet.trim() : ""; + this.requestSignature = + typeof options.requestSignature === "string" + ? options.requestSignature + : JSON.stringify({ + url: this.sanitizedUrl, + magnet: this.trimmedMagnet, + }); if (typeof service.deriveTorrentPlaybackConfig === "function") { this.playbackConfig = service.deriveTorrentPlaybackConfig({ @@ -273,6 +282,24 @@ class PlaybackSession extends SimpleEventEmitter { this.magnetProvided = !!this.playbackConfig.provided; } + isActive() { + return !this.finished; + } + + matchesRequestSignature(signature) { + if (!this.isActive()) { + return false; + } + if (typeof signature !== "string" || !signature) { + return false; + } + return signature === this.requestSignature; + } + + getResult() { + return this.result; + } + getPlaybackConfig() { return this.playbackConfig; } @@ -362,6 +389,14 @@ class PlaybackSession extends SimpleEventEmitter { } async start() { + if (this.startPromise) { + return this.startPromise; + } + this.startPromise = this.execute(); + return this.startPromise; + } + + async execute() { const { videoElement, waitForCleanup, @@ -376,6 +411,7 @@ class PlaybackSession extends SimpleEventEmitter { } = this.options; this.result = { source: null }; + this.finished = false; const detail = { urlProvided: !!this.sanitizedUrl, @@ -721,6 +757,7 @@ class PlaybackSession extends SimpleEventEmitter { return this.result; } finally { this.cleanupWatchdog(); + this.finished = true; if (this.result) { this.emit("finished", this.result); } From a97ce36c708b6cbc55c7b0e236612d76aeb10add Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:58:28 -0400 Subject: [PATCH 08/10] Start channel playback before metadata fetch --- js/app.js | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/js/app.js b/js/app.js index 10e44621..da6bcf1c 100644 --- a/js/app.js +++ b/js/app.js @@ -7258,7 +7258,10 @@ class Application { try { await accessControl.ensureReady(); } catch (error) { - console.warn("Failed to ensure admin lists were loaded before playback:", error); + console.warn( + "Failed to ensure admin lists were loaded before playback:", + error + ); } const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey; if (!accessControl.canAccess(authorNpub)) { @@ -7281,8 +7284,7 @@ class Application { video.alreadyDecrypted = true; } - let trimmedUrl = - typeof video.url === "string" ? video.url.trim() : ""; + let trimmedUrl = typeof video.url === "string" ? video.url.trim() : ""; if (!trimmedUrl && fallbackUrl) { trimmedUrl = fallbackUrl; } @@ -7404,6 +7406,23 @@ class Application { this.setModalZapVisibility(false); this.resetModalZapState(); + + const magnetInput = + sanitizedMagnet || + decodedMagnetCandidate || + magnetCandidate || + fallbackMagnetForCandidate || + legacyInfoHash || + ""; + + this.showModalWithPoster(this.currentVideo); + this.applyModalLoadingPoster(); + + const playbackPromise = this.playVideoWithFallback({ + url: trimmedUrl, + magnet: magnetInput, + }); + let lightningAddress = ""; let creatorProfile = { name: "Unknown", @@ -7446,18 +7465,12 @@ class Application { }); } - const magnetInput = - sanitizedMagnet || - decodedMagnetCandidate || - magnetCandidate || - fallbackMagnetForCandidate || - legacyInfoHash || - ""; + const playbackResult = + playbackPromise && typeof playbackPromise.then === "function" + ? await playbackPromise + : playbackPromise; - return this.playVideoWithFallback({ - url: trimmedUrl, - magnet: magnetInput, - }); + return playbackResult; } async playVideoWithoutEvent({ From 0843c70123cb701e21e8126b30c385dc30be82b6 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:38:32 -0400 Subject: [PATCH 09/10] Align channel grid playback with main view --- js/channelProfile.js | 608 ++++++++++++++----------------------------- 1 file changed, 189 insertions(+), 419 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index 2bbcd26c..3eef1de3 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -8,8 +8,9 @@ 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"; +import { escapeHTML } from "./utils/domUtils.js"; +import { VideoCard } from "./ui/components/VideoCard.js"; import { calculateZapShares, describeShareType, @@ -1704,9 +1705,97 @@ async function loadUserVideos(pubkey) { container.dataset.hasChannelVideos = "true"; const fragment = document.createDocumentFragment(); const allKnownEventsArray = Array.from(nostrClient.allEvents.values()); + const loadedThumbnails = + app?.loadedThumbnails instanceof Map ? app.loadedThumbnails : null; + const unsupportedBtihMessage = + typeof app?.getUnsupportedBtihMessage === "function" + ? app.getUnsupportedBtihMessage() + : "This magnet link is missing a compatible BitTorrent v1 info hash."; + + const extractPlaybackDetail = (trigger, video) => { + if ( + app?.videoListView && + typeof app.videoListView.extractPlaybackDetail === "function" + ) { + return app.videoListView.extractPlaybackDetail(trigger, video); + } + + const element = trigger instanceof HTMLElement ? trigger : null; + const target = + element?.closest("[data-play-url],[data-play-magnet]") || element; + + const rawUrlValue = + (target?.dataset && typeof target.dataset.playUrl === "string" + ? target.dataset.playUrl + : null) ?? target?.getAttribute?.("data-play-url") ?? ""; + const rawMagnetValue = + (target?.dataset && typeof target.dataset.playMagnet === "string" + ? target.dataset.playMagnet + : null) ?? target?.getAttribute?.("data-play-magnet") ?? ""; + + let url = ""; + if (rawUrlValue) { + try { + url = decodeURIComponent(rawUrlValue); + } catch (error) { + url = rawUrlValue; + } + } + + const magnet = typeof rawMagnetValue === "string" ? rawMagnetValue : ""; + const videoId = + target?.dataset?.videoId || + target?.getAttribute?.("data-video-id") || + video?.id || + ""; + + return { videoId, url, magnet, video }; + }; + + const playbackHandler = + typeof app?.videoListViewPlaybackHandler === "function" + ? app.videoListViewPlaybackHandler.bind(app) + : null; + + const startPlayback = (detail) => { + if (playbackHandler) { + playbackHandler(detail); + return; + } + + if (detail.videoId && typeof app?.playVideoByEventId === "function") { + Promise.resolve( + app.playVideoByEventId(detail.videoId, { + url: detail.url, + magnet: detail.magnet, + title: detail.video?.title, + description: detail.video?.description, + }) + ).catch((error) => { + console.error("Failed to play channel video via event id:", error); + if (typeof app?.playVideoWithFallback === "function") { + app.playVideoWithFallback({ + url: detail.url, + magnet: detail.magnet, + }); + } + }); + return; + } + + if (typeof app?.playVideoWithFallback === "function") { + Promise.resolve( + app.playVideoWithFallback({ url: detail.url, magnet: detail.magnet }) + ).catch((error) => { + console.error( + "Failed to start fallback playback for channel video:", + error + ); + }); + } + }; videos.forEach((video, index) => { - // Decrypt if user owns a private video if ( video.isPrivate && video.pubkey === nostrClient.pubkey && @@ -1716,304 +1805,118 @@ async function loadUserVideos(pubkey) { video.alreadyDecrypted = true; } - // Ensure the global videos map is kept up to date so delegated handlers - // have the freshest metadata for this event. app?.videosMap?.set(video.id, video); - // Check if user can edit const canEdit = video.pubkey === app.pubkey; let hasOlder = false; if (canEdit && video.videoRootId) { hasOlder = app.hasOlderVersion(video, allKnownEventsArray); } - const revertButton = hasOlder - ? ` - - ` - : ""; - - let gearMenu = ""; - if (canEdit) { - gearMenu = ` -- ${new Date(video.created_at * 1000).toLocaleString()} -
-