From 69942232008851259df21ca73450e9832e0d87f2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:17:00 -0400 Subject: [PATCH] Refresh all video grids after moderation updates --- js/app.js | 99 ++++++++++++++++++++--- js/channelProfile.js | 117 ++++++++++++++++++---------- js/ui/moreMenuController.js | 46 +++++------ tests/more-menu-controller.test.mjs | 71 +++++++---------- 4 files changed, 211 insertions(+), 122 deletions(-) diff --git a/js/app.js b/js/app.js index bb366e68..a0afb89d 100644 --- a/js/app.js +++ b/js/app.js @@ -44,6 +44,7 @@ import WatchHistoryController from "./ui/watchHistoryController.js"; import WatchHistoryTelemetry from "./services/watchHistoryTelemetry.js"; import { getSidebarLoadingMarkup } from "./sidebarLoading.js"; import { subscriptions } from "./subscriptions.js"; +import { refreshActiveChannelVideoGrid } from "./channelProfile.js"; import { isWatchHistoryDebugEnabled } from "./watchHistoryDebug.js"; import { devLogger, userLogger } from "./utils/logger.js"; import createPopover from "./ui/overlay/popoverEngine.js"; @@ -827,6 +828,7 @@ class Application { handleEnsurePresenceAction: (payload) => this.handleEnsurePresenceAction(payload), loadVideos: () => this.loadVideos(), + refreshAllVideoGrids: (options) => this.refreshAllVideoGrids(options), onUserBlocksUpdated: () => { if (this.profileController) { try { @@ -2067,29 +2069,102 @@ class Application { } } - this.loadVideos(true).catch((error) => { - devLogger.error("Failed to refresh videos after admin update:", error); + this.refreshAllVideoGrids({ + reason: "access-control-update", + forceMainReload: true, + }).catch((error) => { + devLogger.error("Failed to refresh video grids after admin update:", error); }); window.dispatchEvent(new CustomEvent("bitvid:access-control-updated")); } - async onVideosShouldRefresh({ reason } = {}) { + async refreshAllVideoGrids({ reason, forceMainReload = false } = {}) { + const normalizedReason = + typeof reason === "string" && reason.trim() ? reason.trim() : undefined; + + if (typeof moderationService?.awaitUserBlockRefresh === "function") { + try { + await moderationService.awaitUserBlockRefresh(); + } catch (error) { + const contextMessage = normalizedReason + ? ` before ${normalizedReason}` + : ""; + devLogger.warn( + `Failed to sync moderation summaries${contextMessage}:`, + error, + ); + } + } + try { - if (typeof moderationService?.awaitUserBlockRefresh === "function") { - try { - await moderationService.awaitUserBlockRefresh(); - } catch (error) { + await this.loadVideos(forceMainReload); + } catch (error) { + const contextMessage = normalizedReason + ? ` after ${normalizedReason}` + : ""; + devLogger.error( + `Failed to refresh recent videos${contextMessage}:`, + error, + ); + } + + const refreshTasks = []; + + if (typeof subscriptions?.refreshActiveFeed === "function") { + const subscriptionPromise = subscriptions + .refreshActiveFeed({ reason: normalizedReason }) + .catch((error) => { + const contextMessage = normalizedReason + ? ` after ${normalizedReason}` + : ""; devLogger.warn( - "Failed to sync moderation summaries before refreshing videos:", + `Failed to refresh subscriptions grid${contextMessage}:`, error, ); - } - } + }); + refreshTasks.push(subscriptionPromise); + } - await this.loadVideos(true); + if (typeof refreshActiveChannelVideoGrid === "function") { + try { + const maybePromise = refreshActiveChannelVideoGrid({ + reason: normalizedReason, + }); + if (maybePromise && typeof maybePromise.then === "function") { + refreshTasks.push( + maybePromise.catch((error) => { + const contextMessage = normalizedReason + ? ` after ${normalizedReason}` + : ""; + devLogger.warn( + `Failed to refresh channel grid${contextMessage}:`, + error, + ); + }), + ); + } + } catch (error) { + const contextMessage = normalizedReason + ? ` after ${normalizedReason}` + : ""; + devLogger.warn( + `Failed to trigger channel grid refresh${contextMessage}:`, + error, + ); + } + } + + if (refreshTasks.length) { + await Promise.allSettled(refreshTasks); + } + } + + async onVideosShouldRefresh({ reason } = {}) { + try { + await this.refreshAllVideoGrids({ reason, forceMainReload: true }); } catch (error) { const context = reason ? ` after ${reason}` : ""; - devLogger.error(`Failed to refresh videos${context}:`, error); + devLogger.error(`Failed to refresh video grids${context}:`, error); } } diff --git a/js/channelProfile.js b/js/channelProfile.js index a1a10e9e..4f874e34 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -3690,6 +3690,39 @@ async function loadUserProfile(pubkey) { * Fetches and displays this user's videos (kind=30078). * Filters out older overshadowed notes, blacklisted, etc. */ +export function refreshActiveChannelVideoGrid({ reason } = {}) { + if (typeof window === "undefined") { + return Promise.resolve(); + } + + const hashParams = new URLSearchParams(window.location.hash.slice(1)); + if (hashParams.get("view") !== "channel-profile") { + return Promise.resolve(); + } + + if (!currentChannelHex) { + return Promise.resolve(); + } + + const activeNpub = hashParams.get("npub"); + if (currentChannelNpub && activeNpub && activeNpub !== currentChannelNpub) { + return Promise.resolve(); + } + + try { + const result = loadUserVideos(currentChannelHex); + return result instanceof Promise ? result : Promise.resolve(result); + } catch (error) { + userLogger.warn( + reason + ? `Failed to trigger channel refresh after ${reason}:` + : "Failed to trigger channel refresh:", + error, + ); + return Promise.resolve(); + } +} + async function loadUserVideos(pubkey) { const app = getApp(); const container = document.getElementById("channelVideoList"); @@ -3835,34 +3868,35 @@ async function loadUserVideos(pubkey) { } } -window.addEventListener("bitvid:access-control-updated", () => { - const hashParams = new URLSearchParams(window.location.hash.slice(1)); - if (hashParams.get("view") !== "channel-profile") { - return; - } +if (typeof window !== "undefined" && typeof window.addEventListener === "function") { + window.addEventListener("bitvid:access-control-updated", () => { + const hashParams = new URLSearchParams(window.location.hash.slice(1)); + if (hashParams.get("view") !== "channel-profile") { + return; + } - if (!currentChannelHex) { - return; - } + if (!currentChannelHex) { + return; + } - const activeNpub = hashParams.get("npub"); - if (currentChannelNpub && activeNpub && activeNpub !== currentChannelNpub) { - return; - } + const activeNpub = hashParams.get("npub"); + if (currentChannelNpub && activeNpub && activeNpub !== currentChannelNpub) { + return; + } - loadUserVideos(currentChannelHex).catch((error) => { - userLogger.error( - "Failed to refresh channel videos after admin update:", - error - ); + loadUserVideos(currentChannelHex).catch((error) => { + userLogger.error( + "Failed to refresh channel videos after admin update:", + error + ); + }); }); -}); -window.addEventListener("bitvid:auth-changed", (event) => { - const hashParams = new URLSearchParams(window.location.hash.slice(1)); - if (hashParams.get("view") !== "channel-profile") { - return; - } + window.addEventListener("bitvid:auth-changed", (event) => { + const hashParams = new URLSearchParams(window.location.hash.slice(1)); + if (hashParams.get("view") !== "channel-profile") { + return; + } if (!currentChannelHex) { return; @@ -3896,27 +3930,28 @@ window.addEventListener("bitvid:auth-changed", (event) => { return; } - if (detail.status === "login") { - zapInFlight = false; - resetZapRetryState(); - setZapStatus("", "neutral"); - if (channelZapPendingOpen) { - channelZapPendingOpen = false; - const zapButton = getChannelZapButton(); - const requiresLogin = - zapButton?.dataset?.requiresLogin === "true" || - zapButton?.hasAttribute("hidden"); - if (zapButton && !requiresLogin) { - const opened = openZapControls({ focus: true }); - if (!opened) { - setupZapButton({ force: true }); - openZapControls({ focus: true }); + if (detail.status === "login") { + zapInFlight = false; + resetZapRetryState(); + setZapStatus("", "neutral"); + if (channelZapPendingOpen) { + channelZapPendingOpen = false; + const zapButton = getChannelZapButton(); + const requiresLogin = + zapButton?.dataset?.requiresLogin === "true" || + zapButton?.hasAttribute("hidden"); + if (zapButton && !requiresLogin) { + const opened = openZapControls({ focus: true }); + if (!opened) { + setupZapButton({ force: true }); + openZapControls({ focus: true }); + } } } + return; } - return; - } -}); + }); +} /** * Keep only the newest version of each video root. diff --git a/js/ui/moreMenuController.js b/js/ui/moreMenuController.js index 641f4588..36a303bf 100644 --- a/js/ui/moreMenuController.js +++ b/js/ui/moreMenuController.js @@ -69,6 +69,14 @@ export default class MoreMenuController { onUserBlocksUpdated: callbacks.onUserBlocksUpdated || (() => {}), }; + this.callbacks.refreshAllVideoGrids = + typeof callbacks.refreshAllVideoGrids === "function" + ? (options) => callbacks.refreshAllVideoGrids(options) + : (options = {}) => + Promise.resolve( + this.callbacks.loadVideos(Boolean(options?.forceMainReload)), + ).catch(() => {}); + this.moreMenuGlobalHandlerBound = false; this.videoListView = null; @@ -1198,20 +1206,14 @@ export default class MoreMenuController { ); } try { - await this.callbacks.loadVideos(); - } catch (loadError) { - if (this.isDevMode) { - userLogger.warn("[MoreMenu] Failed to reload videos after muting", loadError); - } - } - try { - await this.subscriptions?.refreshActiveFeed?.({ + await this.callbacks.refreshAllVideoGrids({ reason: "viewer-mute-update", + forceMainReload: true, }); } catch (refreshError) { if (this.isDevMode) { userLogger.warn( - "[Subscriptions] Failed to refresh after viewer mute update:", + "[MoreMenu] Failed to refresh video grids after viewer mute update:", refreshError, ); } @@ -1272,23 +1274,14 @@ export default class MoreMenuController { ); } try { - await this.callbacks.loadVideos(); - } catch (loadError) { - if (this.isDevMode) { - userLogger.warn( - "[MoreMenu] Failed to reload videos after unmuting", - loadError, - ); - } - } - try { - await this.subscriptions?.refreshActiveFeed?.({ + await this.callbacks.refreshAllVideoGrids({ reason: "viewer-mute-update", + forceMainReload: true, }); } catch (refreshError) { if (this.isDevMode) { userLogger.warn( - "[Subscriptions] Failed to refresh after viewer mute removal:", + "[MoreMenu] Failed to refresh video grids after viewer mute removal:", refreshError, ); } @@ -1369,13 +1362,14 @@ export default class MoreMenuController { if (result?.ok) { this.callbacks.showSuccess("Creator added to the blacklist."); try { - await this.subscriptions?.refreshActiveFeed?.({ + await this.callbacks.refreshAllVideoGrids({ reason: "admin-blacklist-update", + forceMainReload: true, }); } catch (error) { if (this.isDevMode) { userLogger.warn( - "[Subscriptions] Failed to refresh after blacklist update:", + "[MoreMenu] Failed to refresh video grids after blacklist update:", error, ); } @@ -1478,15 +1472,15 @@ export default class MoreMenuController { } } - await this.callbacks.loadVideos(); try { - await this.subscriptions?.refreshActiveFeed?.({ + await this.callbacks.refreshAllVideoGrids({ reason: "user-block-update", + forceMainReload: true, }); } catch (error) { if (this.isDevMode) { userLogger.warn( - "[Subscriptions] Failed to refresh after user block update:", + "[MoreMenu] Failed to refresh video grids after user block update:", error, ); } diff --git a/tests/more-menu-controller.test.mjs b/tests/more-menu-controller.test.mjs index 11b8d208..b2f5d65e 100644 --- a/tests/more-menu-controller.test.mjs +++ b/tests/more-menu-controller.test.mjs @@ -47,15 +47,9 @@ test('blacklist-author requires moderator access and refreshes subscriptions', a }; let refreshedWith = null; - const subscriptions = { - refreshActiveFeed: async (args) => { - refreshedWith = args; - }, - }; const controller = new MoreMenuController({ accessControl, - subscriptions, callbacks: { getCurrentUserNpub: () => 'npub1actor', getCurrentVideo: () => ({ pubkey: 'authorhex' }), @@ -67,6 +61,9 @@ test('blacklist-author requires moderator access and refreshes subscriptions', a calls.push(['success', message]); }, canCurrentUserManageBlacklist: () => true, + refreshAllVideoGrids: async (args) => { + refreshedWith = args; + }, }, }); @@ -80,7 +77,10 @@ test('blacklist-author requires moderator access and refreshes subscriptions', a ['add', 'npub1actor', 'npub:authorhex'], ['success', 'Creator added to the blacklist.'], ]); - assert.deepEqual(refreshedWith, { reason: 'admin-blacklist-update' }); + assert.deepEqual(refreshedWith, { + reason: 'admin-blacklist-update', + forceMainReload: true, + }); }); test('blacklist-author shows error when no moderator session is available', async () => { @@ -117,12 +117,6 @@ test('block-author updates user blocks, reloads videos, and refreshes feeds', as }, }; - const subscriptions = { - refreshActiveFeed: async (args) => { - events.push(['refreshActiveFeed', args]); - }, - }; - const moderationStub = { awaitUserBlockRefresh: async () => { events.push(['awaitUserBlockRefresh']); @@ -131,7 +125,6 @@ test('block-author updates user blocks, reloads videos, and refreshes feeds', as const controller = new MoreMenuController({ userBlocks, - subscriptions, moderationService: moderationStub, callbacks: { getCurrentUserPubkey: () => 'actorhex', @@ -143,12 +136,12 @@ test('block-author updates user blocks, reloads videos, and refreshes feeds', as showSuccess: (message) => { events.push(['success', message]); }, - loadVideos: async () => { - events.push(['loadVideos']); - }, onUserBlocksUpdated: () => { events.push(['refreshBlockedList']); }, + refreshAllVideoGrids: async (args) => { + events.push(['refreshAllVideoGrids', args]); + }, }, }); @@ -163,8 +156,10 @@ test('block-author updates user blocks, reloads videos, and refreshes feeds', as ['success', "Creator blocked. You won't see their videos anymore."], ['refreshBlockedList'], ['awaitUserBlockRefresh'], - ['loadVideos'], - ['refreshActiveFeed', { reason: 'user-block-update' }], + [ + 'refreshAllVideoGrids', + { reason: 'user-block-update', forceMainReload: true }, + ], ]); }); @@ -181,21 +176,11 @@ test('mute-author updates viewer mute list and refreshes feeds', async () => { isAuthorMutedByViewer: () => false, }; - const subscriptions = { - refreshActiveFeed: async (args) => { - events.push(['refreshActiveFeed', args]); - }, - }; - const controller = new MoreMenuController({ moderationService: moderationStub, - subscriptions, callbacks: { getCurrentUserPubkey: () => 'viewerhex', getCurrentVideo: () => ({ pubkey: 'fallbackhex' }), - loadVideos: async () => { - events.push(['loadVideos']); - }, showSuccess: (message) => { events.push(['success', message]); }, @@ -203,6 +188,9 @@ test('mute-author updates viewer mute list and refreshes feeds', async () => { events.push(['error', message]); }, safeDecodeNpub: () => '', + refreshAllVideoGrids: async (args) => { + events.push(['refreshAllVideoGrids', args]); + }, }, }); @@ -213,8 +201,10 @@ test('mute-author updates viewer mute list and refreshes feeds', async () => { assert.deepEqual(events, [ ['addMute', targetHex], ['success', 'Creator muted. Their videos will be downranked in your feeds.'], - ['loadVideos'], - ['refreshActiveFeed', { reason: 'viewer-mute-update' }], + [ + 'refreshAllVideoGrids', + { reason: 'viewer-mute-update', forceMainReload: true }, + ], ]); }); @@ -231,21 +221,11 @@ test('unmute-author removes creators from viewer mute list', async () => { isAuthorMutedByViewer: () => true, }; - const subscriptions = { - refreshActiveFeed: async (args) => { - events.push(['refreshActiveFeed', args]); - }, - }; - const controller = new MoreMenuController({ moderationService: moderationStub, - subscriptions, callbacks: { getCurrentUserPubkey: () => 'viewerhex', getCurrentVideo: () => ({ pubkey: 'fallbackhex' }), - loadVideos: async () => { - events.push(['loadVideos']); - }, showSuccess: (message) => { events.push(['success', message]); }, @@ -253,6 +233,9 @@ test('unmute-author removes creators from viewer mute list', async () => { events.push(['error', message]); }, safeDecodeNpub: () => '', + refreshAllVideoGrids: async (args) => { + events.push(['refreshAllVideoGrids', args]); + }, }, }); @@ -263,7 +246,9 @@ test('unmute-author removes creators from viewer mute list', async () => { assert.deepEqual(events, [ ['removeMute', targetHex], ['success', 'Creator removed from your mute list.'], - ['loadVideos'], - ['refreshActiveFeed', { reason: 'viewer-mute-update' }], + [ + 'refreshAllVideoGrids', + { reason: 'viewer-mute-update', forceMainReload: true }, + ], ]); });