refactor(ai): extract videoSettingsMenuController from js/app.js (agent)

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-01-29 03:19:06 +00:00
1 parent c3f96c8819
commit aabed9aed2
4 files changed
+308 -129

No files matched your search

+10 -128
View File
@@ -73,7 +73,6 @@ import {
import { isWatchHistoryDebugEnabled } from "./watchHistoryDebug.js";
import { devLogger, userLogger } from "./utils/logger.js";
import createPopover from "./ui/overlay/popoverEngine.js";
import { createVideoSettingsMenuPanel } from "./ui/components/videoMenuRenderers.js";
import moderationService from "./services/moderationService.js";
import { sanitizeRelayList } from "./nostr/nip46Client.js";
import { buildDmRelayListEvent, buildShareEvent } from "./nostrEventSchemas.js";
@@ -7562,139 +7561,17 @@ class Application {
return true;
}
ensureSettingsPopover(detail = {}) {
const trigger = detail.trigger || null;
if (!trigger) {
return null;
}
let entry = this.videoSettingsPopovers.get(trigger);
if (!entry) {
entry = {
trigger,
context: {
card: detail.card || null,
video: detail.video || null,
index: Number.isFinite(detail.index) ? Math.floor(detail.index) : 0,
capabilities: detail.capabilities || {},
restoreFocusOnClose: detail.restoreFocus !== false,
},
popover: null,
};
const render = ({ document: documentRef, close }) => {
const panel = createVideoSettingsMenuPanel({
document: documentRef,
video: entry.context.video,
index: entry.context.index,
capabilities: entry.context.capabilities,
designSystem: this.designSystemContext,
});
if (!panel) {
return null;
}
const buttons = panel.querySelectorAll("button[data-action]");
buttons.forEach((button) => {
button.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
const action = button.dataset.action || "";
const handled = entry.context.card?.handleSettingsMenuAction?.(
action,
{ event },
);
if (!handled && this.isDevMode) {
userLogger.warn(`[SettingsMenu] Unhandled action: ${action}`);
}
close();
});
});
return panel;
};
const documentRef =
trigger.ownerDocument ||
(typeof document !== "undefined" ? document : null);
const popover = createPopover(trigger, render, {
document: documentRef,
placement: "bottom-end",
});
const originalDestroy = popover.destroy?.bind(popover);
if (typeof originalDestroy === "function") {
popover.destroy = (...args) => {
originalDestroy(...args);
if (this.videoSettingsPopovers.get(trigger) === entry) {
this.videoSettingsPopovers.delete(trigger);
}
};
}
entry.popover = popover;
this.videoSettingsPopovers.set(trigger, entry);
}
entry.context = {
...entry.context,
card: detail.card || entry.context.card,
video: detail.video || entry.context.video,
index: Number.isFinite(detail.index)
? Math.floor(detail.index)
: entry.context.index,
capabilities: detail.capabilities || entry.context.capabilities,
restoreFocusOnClose: detail.restoreFocus !== false,
};
return entry;
}
requestVideoSettingsMenu(detail = {}) {
const entry = this.ensureSettingsPopover(detail);
if (!entry?.popover) {
return;
if (this.videoSettingsMenuController) {
this.videoSettingsMenuController.requestMenu(detail);
}
if (typeof entry.popover.isOpen === "function" && entry.popover.isOpen()) {
entry.popover.close({
restoreFocus: entry.context.restoreFocusOnClose !== false,
});
return;
}
entry.popover
.open()
.catch((error) =>
userLogger.error("[SettingsMenu] Failed to open popover:", error),
);
}
closeVideoSettingsMenu(detail = {}) {
const trigger = detail.trigger || null;
const restoreFocus = detail.restoreFocus !== false;
if (trigger) {
const entry = this.videoSettingsPopovers.get(trigger);
if (entry?.popover && typeof entry.popover.close === "function") {
return entry.popover.close({ restoreFocus });
}
return false;
if (this.videoSettingsMenuController) {
return this.videoSettingsMenuController.closeMenu(detail);
}
let closed = false;
this.videoSettingsPopovers.forEach((entry) => {
if (entry?.popover && typeof entry.popover.close === "function") {
const result = entry.popover.close({ restoreFocus });
closed = closed || result;
}
});
return closed;
return false;
}
ensureTagPreferencePopover(detail) {
@@ -10093,6 +9970,11 @@ class Application {
this.moreMenuController = null;
}
if (this.videoSettingsMenuController) {
this.videoSettingsMenuController.destroy();
this.videoSettingsMenuController = null;
}
if (this.profileController) {
if (typeof this.profileController.destroy === "function") {
try {
+5 -1
View File
@@ -34,6 +34,7 @@ import ProfileModalController from "./profileModalController.js";
import LoginModalController from "./loginModalController.js";
import { VideoListView } from "./views/VideoListView.js";
import MoreMenuController from "./moreMenuController.js";
import VideoSettingsMenuController from "./videoSettingsMenuController.js";
import AppChromeController from "./appChromeController.js";
import { getSidebarLoadingMarkup } from "../sidebarLoading.js";
import { isWatchHistoryDebugEnabled } from "../watchHistoryDebug.js";
@@ -671,7 +672,10 @@ export default class ApplicationBootstrap {
},
});
app.moreMenuController.setVideoModal(app.videoModal);
app.videoSettingsPopovers = new Map();
app.videoSettingsMenuController = new VideoSettingsMenuController({
designSystem: app.designSystemContext,
isDevMode,
});
app.tagPreferencePopovers = new Map();
app.subscriptionsLink = null;
+171
View File
@@ -0,0 +1,171 @@
import createPopoverDefault from "./overlay/popoverEngine.js";
import { createVideoSettingsMenuPanel as createVideoSettingsMenuPanelDefault } from "./components/videoMenuRenderers.js";
import { userLogger } from "../utils/logger.js";
import { normalizeDesignSystemContext } from "../designSystem.js";
export default class VideoSettingsMenuController {
constructor(options = {}) {
const {
designSystem = null,
isDevMode = false,
createPopover = createPopoverDefault,
createVideoSettingsMenuPanel = createVideoSettingsMenuPanelDefault,
} = options;
this.createPopover = createPopover;
this.createVideoSettingsMenuPanel = createVideoSettingsMenuPanel;
this.designSystem = normalizeDesignSystemContext(designSystem);
this.isDevMode = Boolean(isDevMode);
this.popovers = new Map();
}
ensurePopover(detail = {}) {
const trigger = detail.trigger || null;
if (!trigger) {
return null;
}
let entry = this.popovers.get(trigger);
if (!entry) {
entry = {
trigger,
context: {
card: detail.card || null,
video: detail.video || null,
index: Number.isFinite(detail.index) ? Math.floor(detail.index) : 0,
capabilities: detail.capabilities || {},
restoreFocusOnClose: detail.restoreFocus !== false,
},
popover: null,
};
const render = ({ document: documentRef, close }) => {
const panel = this.createVideoSettingsMenuPanel({
document: documentRef,
video: entry.context.video,
index: entry.context.index,
capabilities: entry.context.capabilities,
designSystem: this.designSystem,
});
if (!panel) {
return null;
}
const buttons = panel.querySelectorAll("button[data-action]");
buttons.forEach((button) => {
button.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
const action = button.dataset.action || "";
const handled = entry.context.card?.handleSettingsMenuAction?.(
action,
{ event },
);
if (!handled && this.isDevMode) {
userLogger.warn(`[SettingsMenu] Unhandled action: ${action}`);
}
close();
});
});
return panel;
};
const documentRef =
trigger.ownerDocument ||
(typeof document !== "undefined" ? document : null);
const popover = this.createPopover(trigger, render, {
document: documentRef,
placement: "bottom-end",
});
const originalDestroy = popover.destroy?.bind(popover);
if (typeof originalDestroy === "function") {
popover.destroy = (...args) => {
originalDestroy(...args);
if (this.popovers.get(trigger) === entry) {
this.popovers.delete(trigger);
}
};
}
entry.popover = popover;
this.popovers.set(trigger, entry);
}
entry.context = {
...entry.context,
card: detail.card || entry.context.card,
video: detail.video || entry.context.video,
index: Number.isFinite(detail.index)
? Math.floor(detail.index)
: entry.context.index,
capabilities: detail.capabilities || entry.context.capabilities,
restoreFocusOnClose: detail.restoreFocus !== false,
};
return entry;
}
requestMenu(detail = {}) {
const entry = this.ensurePopover(detail);
if (!entry?.popover) {
return;
}
if (typeof entry.popover.isOpen === "function" && entry.popover.isOpen()) {
entry.popover.close({
restoreFocus: entry.context.restoreFocusOnClose !== false,
});
return;
}
entry.popover
.open()
.catch((error) =>
userLogger.error("[SettingsMenu] Failed to open popover:", error),
);
}
closeMenu(detail = {}) {
const trigger = detail.trigger || null;
const restoreFocus = detail.restoreFocus !== false;
if (trigger) {
const entry = this.popovers.get(trigger);
if (entry?.popover && typeof entry.popover.close === "function") {
return entry.popover.close({ restoreFocus });
}
return false;
}
return this.closeAll({ restoreFocus });
}
closeAll(options = {}) {
let closed = false;
const restoreFocus = options?.restoreFocus !== false;
this.popovers.forEach((entry) => {
if (entry?.popover && typeof entry.popover.close === "function") {
const result = entry.popover.close({ restoreFocus });
closed = closed || result;
}
});
return closed;
}
destroy() {
this.popovers.forEach((entry) => {
if (entry?.popover && typeof entry.popover.destroy === "function") {
entry.popover.destroy();
}
});
this.popovers.clear();
}
}
@@ -0,0 +1,122 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import VideoSettingsMenuController from '../js/ui/videoSettingsMenuController.js';
test('VideoSettingsMenuController - requestMenu opens popover', async () => {
let popoverOpened = false;
const mockPopover = {
open: async () => { popoverOpened = true; },
close: () => {},
destroy: () => {},
isOpen: () => false,
};
const createPopover = () => mockPopover;
const createVideoSettingsMenuPanel = () => ({ querySelectorAll: () => [] });
const controller = new VideoSettingsMenuController({
createPopover,
createVideoSettingsMenuPanel,
});
const trigger = { ownerDocument: {} };
await controller.requestMenu({ trigger });
assert.equal(popoverOpened, true);
});
test('VideoSettingsMenuController - closeMenu closes popover', async () => {
let popoverClosed = false;
const mockPopover = {
open: async () => {},
close: () => { popoverClosed = true; return true; },
destroy: () => {},
isOpen: () => true,
};
const createPopover = () => mockPopover;
const createVideoSettingsMenuPanel = () => ({ querySelectorAll: () => [] });
const controller = new VideoSettingsMenuController({
createPopover,
createVideoSettingsMenuPanel,
});
const trigger = { ownerDocument: {} };
controller.requestMenu({ trigger }); // creates entry, calls close because isOpen is true
// reset closed flag because requestMenu closed it
popoverClosed = false;
controller.closeMenu({ trigger });
assert.equal(popoverClosed, true);
});
test('VideoSettingsMenuController - requestMenu toggles if open', async () => {
let popoverClosed = false;
const mockPopover = {
open: async () => {},
close: () => { popoverClosed = true; return true; },
destroy: () => {},
isOpen: () => true,
};
const createPopover = () => mockPopover;
const createVideoSettingsMenuPanel = () => ({ querySelectorAll: () => [] });
const controller = new VideoSettingsMenuController({
createPopover,
createVideoSettingsMenuPanel,
});
const trigger = { ownerDocument: {} };
controller.requestMenu({ trigger }); // closes because isOpen=true
popoverClosed = false;
controller.requestMenu({ trigger }); // closes again
assert.equal(popoverClosed, true);
});
test('VideoSettingsMenuController - closeAll closes all popovers', async () => {
let closeCount = 0;
const createPopover = () => {
let open = false;
return {
open: async () => { open = true; },
close: () => {
if (open) {
closeCount++;
open = false;
return true;
}
return false;
},
destroy: () => {},
isOpen: () => open,
};
};
const createVideoSettingsMenuPanel = () => ({ querySelectorAll: () => [] });
const controller = new VideoSettingsMenuController({
createPopover,
createVideoSettingsMenuPanel,
});
const trigger1 = { ownerDocument: {}, id: 1 };
const trigger2 = { ownerDocument: {}, id: 2 };
// These will open them because isOpen starts as false
controller.requestMenu({ trigger: trigger1 });
controller.requestMenu({ trigger: trigger2 });
// Wait a tick for async open
await Promise.resolve();
controller.closeAll();
assert.equal(closeCount, 2);
});