From 62dcc57e646a1fe14cc914b6e90d1224b0c02c8a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 03:21:44 +0000 Subject: [PATCH] refactor(ai): extract EditModalController from js/app.js (agent) --- js/app.js | 109 +++++----------- js/ui/editModalController.js | 119 ++++++++++++++++++ tests/unit/editModalController.test.mjs | 160 ++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 75 deletions(-) create mode 100644 js/ui/editModalController.js create mode 100644 tests/unit/editModalController.test.mjs diff --git a/js/app.js b/js/app.js index 8369a20b..d659f062 100644 --- a/js/app.js +++ b/js/app.js @@ -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); } } diff --git a/js/ui/editModalController.js b/js/ui/editModalController.js new file mode 100644 index 00000000..5b762ca0 --- /dev/null +++ b/js/ui/editModalController.js @@ -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 }); + } + } + } +} diff --git a/tests/unit/editModalController.test.mjs b/tests/unit/editModalController.test.mjs new file mode 100644 index 00000000..c227ddf3 --- /dev/null +++ b/tests/unit/editModalController.test.mjs @@ -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 }); + }); + }); +});