mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
refactor(ai): extract EditModalController from js/app.js (agent)
This commit is contained in:
1 parent
87c494c1f3
commit
62dcc57e64
3 files changed
+313
-75
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user