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

This commit is contained in:
google-labs-jules[bot] committed 2026-01-26 03:21:44 +00:00
1 parent 87c494c1f3
commit 62dcc57e64
3 files changed
+313 -75

No files matched your search

+34 -75
View File
@@ -112,6 +112,7 @@ import {
closeStaticModal,
} from "./ui/components/staticModalAccessibility.js";
import LoginModalController from "./ui/loginModalController.js";
import EditModalController from "./ui/editModalController.js";
import TagPreferenceMenuController from "./ui/tagPreferenceMenuController.js";
import ReactionController from "./ui/reactionController.js";
import { pointerArrayToKey } from "./utils/pointer.js";
@@ -390,6 +391,35 @@ class Application {
this.initializeModerationActionController();
this.initializeSimilarContentController();
this.editModalController = new EditModalController({
services: {
nostrService: {
fetchVideos: (...args) => this.nostrService.fetchVideos(...args),
handleEditVideoSubmit: (...args) =>
this.nostrService.handleEditVideoSubmit(...args),
},
},
state: {
getPubkey: () => this.pubkey,
getBlacklistedEventIds: () => this.blacklistedEventIds,
getVideosMap: () => this.videosMap,
},
ui: {
getEditModal: () => this.editModal,
showError: (msg) => this.showError(msg),
showSuccess: (msg) => this.showSuccess(msg),
},
callbacks: {
loadVideos: () => this.loadVideos(),
forceRefreshAllProfiles: () => this.forceRefreshAllProfiles(),
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
},
helpers: {
normalizeActionTarget: (t) => this.normalizeActionTarget(t),
resolveVideoActionTarget: (opts) => this.resolveVideoActionTarget(opts),
},
});
this.handleShareNostrSignerChange = () => {
this.updateShareNostrAuthState({ reason: "signer-change" });
};
@@ -7775,85 +7805,14 @@ class Application {
* Handle "Edit Video" from gear menu.
*/
async handleEditModalSubmit(event) {
const detail = event?.detail || {};
const { originalEvent, updatedData } = detail;
if (!originalEvent || !updatedData) {
return;
}
if (!this.pubkey) {
this.showError("Please login to edit videos.");
if (this.editModal?.setSubmitState) {
this.editModal.setSubmitState({ pending: false });
}
return;
}
try {
await this.nostrService.handleEditVideoSubmit({
originalEvent,
updatedData,
pubkey: this.pubkey,
});
await this.loadVideos();
this.videosMap.clear();
this.showSuccess("Video updated successfully!");
if (this.editModal?.setSubmitState) {
this.editModal.setSubmitState({ pending: false });
}
this.editModal.close();
this.forceRefreshAllProfiles();
} catch (error) {
devLogger.error("Failed to edit video:", error);
this.showError("Failed to edit video. Please try again.");
if (this.editModal?.setSubmitState) {
this.editModal.setSubmitState({ pending: false });
}
if (this.editModalController) {
return this.editModalController.handleSubmit(event);
}
}
async handleEditVideo(target) {
try {
const normalizedTarget = this.normalizeActionTarget(target);
const { triggerElement } = normalizedTarget;
const latestVideos = await this.nostrService.fetchVideos({
blacklistedEventIds: this.blacklistedEventIds,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
});
const video = await this.resolveVideoActionTarget({
...normalizedTarget,
preloadedList: latestVideos,
});
// 2) Basic ownership checks
if (!this.pubkey) {
this.showError("Please login to edit videos.");
return;
}
const userPubkey = (this.pubkey || "").toLowerCase();
const videoPubkey = (video?.pubkey || "").toLowerCase();
if (!video || !videoPubkey || videoPubkey !== userPubkey) {
this.showError("You do not own this video.");
return;
}
try {
await this.editModal.load();
} catch (error) {
devLogger.error("Failed to load edit modal:", error);
this.showError(`Failed to initialize edit modal: ${error.message}`);
return;
}
try {
await this.editModal.open(video, { triggerElement });
} catch (error) {
devLogger.error("Failed to open edit modal:", error);
this.showError("Edit modal is not available right now.");
}
} catch (err) {
devLogger.error("Failed to edit video:", err);
this.showError("Failed to edit video. Please try again.");
if (this.editModalController) {
return this.editModalController.open(target);
}
}
+119
View File
@@ -0,0 +1,119 @@
import { devLogger } from "../utils/logger.js";
export default class EditModalController {
constructor({ services, state, ui, callbacks, helpers }) {
this.services = services;
this.state = state;
this.ui = ui;
this.callbacks = callbacks;
this.helpers = helpers;
}
async open(target) {
try {
const normalizedTarget = this.helpers.normalizeActionTarget(target);
const { triggerElement } = normalizedTarget;
const blacklistedEventIds = this.state.getBlacklistedEventIds();
const isAuthorBlocked = this.callbacks.isAuthorBlocked;
const latestVideos = await this.services.nostrService.fetchVideos({
blacklistedEventIds,
isAuthorBlocked: (pubkey) => isAuthorBlocked(pubkey),
});
const video = await this.helpers.resolveVideoActionTarget({
...normalizedTarget,
preloadedList: latestVideos,
});
// 2) Basic ownership checks
const pubkey = this.state.getPubkey();
if (!pubkey) {
this.ui.showError("Please login to edit videos.");
return;
}
const userPubkey = (pubkey || "").toLowerCase();
const videoPubkey = (video?.pubkey || "").toLowerCase();
if (!video || !videoPubkey || videoPubkey !== userPubkey) {
this.ui.showError("You do not own this video.");
return;
}
const editModal = this.ui.getEditModal();
if (!editModal) {
this.ui.showError("Edit modal is not available right now.");
return;
}
try {
await editModal.load();
} catch (error) {
devLogger.error("Failed to load edit modal:", error);
this.ui.showError(`Failed to initialize edit modal: ${error.message}`);
return;
}
try {
await editModal.open(video, { triggerElement });
} catch (error) {
devLogger.error("Failed to open edit modal:", error);
this.ui.showError("Edit modal is not available right now.");
}
} catch (err) {
devLogger.error("Failed to edit video:", err);
this.ui.showError("Failed to edit video. Please try again.");
}
}
async handleSubmit(event) {
const detail = event?.detail || {};
const { originalEvent, updatedData } = detail;
if (!originalEvent || !updatedData) {
return;
}
const pubkey = this.state.getPubkey();
const editModal = this.ui.getEditModal();
if (!pubkey) {
this.ui.showError("Please login to edit videos.");
if (editModal?.setSubmitState) {
editModal.setSubmitState({ pending: false });
}
return;
}
try {
await this.services.nostrService.handleEditVideoSubmit({
originalEvent,
updatedData,
pubkey,
});
await this.callbacks.loadVideos();
const videosMap = this.state.getVideosMap();
if (videosMap) {
videosMap.clear();
}
this.ui.showSuccess("Video updated successfully!");
if (editModal?.setSubmitState) {
editModal.setSubmitState({ pending: false });
}
if (typeof editModal.close === "function") {
editModal.close();
}
this.callbacks.forceRefreshAllProfiles();
} catch (error) {
devLogger.error("Failed to edit video:", error);
this.ui.showError("Failed to edit video. Please try again.");
if (editModal?.setSubmitState) {
editModal.setSubmitState({ pending: false });
}
}
}
}
+160
View File
@@ -0,0 +1,160 @@
import { test, describe, it, beforeEach, mock } from "node:test";
import assert from "node:assert";
import EditModalController from "../../js/ui/editModalController.js";
describe("EditModalController", () => {
let controller;
let services, state, ui, callbacks, helpers;
beforeEach(() => {
services = {
nostrService: {
fetchVideos: mock.fn(async () => []),
handleEditVideoSubmit: mock.fn(async () => {}),
},
};
state = {
getPubkey: mock.fn(() => "test-pubkey"),
getBlacklistedEventIds: mock.fn(() => new Set()),
getVideosMap: mock.fn(() => new Map()),
};
ui = {
getEditModal: mock.fn(() => ({
load: mock.fn(async () => {}),
open: mock.fn(async () => {}),
setSubmitState: mock.fn(),
close: mock.fn(),
})),
showError: mock.fn(),
showSuccess: mock.fn(),
};
callbacks = {
loadVideos: mock.fn(async () => {}),
forceRefreshAllProfiles: mock.fn(),
isAuthorBlocked: mock.fn(() => false),
};
helpers = {
normalizeActionTarget: mock.fn((t) => ({ triggerElement: null })),
resolveVideoActionTarget: mock.fn(async () => ({
id: "v1",
pubkey: "test-pubkey",
})),
};
controller = new EditModalController({
services,
state,
ui,
callbacks,
helpers,
});
});
describe("open()", () => {
it("should resolve target and open modal if authorized", async () => {
await controller.open("target");
assert.strictEqual(helpers.normalizeActionTarget.mock.callCount(), 1);
assert.strictEqual(services.nostrService.fetchVideos.mock.callCount(), 1);
assert.strictEqual(helpers.resolveVideoActionTarget.mock.callCount(), 1);
assert.strictEqual(ui.getEditModal.mock.callCount(), 1);
const modal = ui.getEditModal.mock.calls[0].result;
assert.strictEqual(modal.load.mock.callCount(), 1);
assert.strictEqual(modal.open.mock.callCount(), 1);
});
it("should show error if not logged in", async () => {
state.getPubkey = mock.fn(() => null);
await controller.open("target");
assert.strictEqual(ui.showError.mock.callCount(), 1);
assert.match(ui.showError.mock.calls[0].arguments[0], /login/i);
const modal = ui.getEditModal.mock.calls[0]?.result;
if (modal) {
assert.strictEqual(modal.open.mock.callCount(), 0);
}
});
it("should show error if user does not own video", async () => {
helpers.resolveVideoActionTarget = mock.fn(async () => ({
id: "v1",
pubkey: "other-pubkey",
}));
await controller.open("target");
assert.strictEqual(ui.showError.mock.callCount(), 1);
assert.match(ui.showError.mock.calls[0].arguments[0], /own/i);
});
it("should handle modal load errors", async () => {
ui.getEditModal = mock.fn(() => ({
load: mock.fn(async () => { throw new Error("Load failed"); }),
open: mock.fn(async () => {}),
}));
await controller.open("target");
assert.strictEqual(ui.showError.mock.callCount(), 1);
assert.match(ui.showError.mock.calls[0].arguments[0], /initialize/i);
});
});
describe("handleSubmit()", () => {
it("should handle successful submission", async () => {
const event = {
detail: {
originalEvent: {},
updatedData: {},
},
};
await controller.handleSubmit(event);
assert.strictEqual(services.nostrService.handleEditVideoSubmit.mock.callCount(), 1);
assert.strictEqual(callbacks.loadVideos.mock.callCount(), 1);
assert.strictEqual(callbacks.forceRefreshAllProfiles.mock.callCount(), 1);
assert.strictEqual(ui.showSuccess.mock.callCount(), 1);
const modal = ui.getEditModal.mock.calls[0].result;
assert.strictEqual(modal.close.mock.callCount(), 1);
});
it("should show error if not logged in", async () => {
state.getPubkey = mock.fn(() => null);
const event = {
detail: {
originalEvent: {},
updatedData: {},
},
};
await controller.handleSubmit(event);
assert.strictEqual(ui.showError.mock.callCount(), 1);
assert.strictEqual(services.nostrService.handleEditVideoSubmit.mock.callCount(), 0);
});
it("should handle submission errors", async () => {
services.nostrService.handleEditVideoSubmit = mock.fn(async () => {
throw new Error("Submit failed");
});
const event = {
detail: {
originalEvent: {},
updatedData: {},
},
};
await controller.handleSubmit(event);
assert.strictEqual(ui.showError.mock.callCount(), 1);
assert.match(ui.showError.mock.calls[0].arguments[0], /failed/i);
const modal = ui.getEditModal.mock.calls[0].result;
assert.strictEqual(modal.setSubmitState.mock.callCount(), 1);
assert.deepStrictEqual(modal.setSubmitState.mock.calls[0].arguments[0], { pending: false });
});
});
});