Merge pull request #721 from PR0M3TH3AN/codex/refresh-video-grids-after-creator-update

Refresh video grids after moderation updates
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-20 13:23:02 -04:00
commit d06956e615
4 files changed
+211 -122

No files matched your search

+87 -12
View File
@@ -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);
}
}
+76 -41
View File
@@ -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.
+20 -26
View File
@@ -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,
);
}
+28 -43
View File
@@ -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 },
],
]);
});