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

Moved `handleRepostAction`, `handleMirrorAction`, and `handleEnsurePresenceAction` from `js/app.js` to `js/ui/engagementController.js`.
This follows the AGENTS.md refactor direction for `bitvidApp` to controllers.

- Extracted logic into `EngagementController`.
- Added unit tests for `EngagementController`.
- Wired `EngagementController` into `js/app.js`.
- Preserved `currentVideoPointer` fallback logic in `derivePointerFromDataset`.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-22 03:36:33 +00:00
1 parent 9b138dc5fa
commit cd9418aa92
3 files changed
+597 -349

No files matched your search

+22 -349
View File
@@ -150,6 +150,7 @@ import {
urlHealthConstants,
} from "./state/cache.js";
import ApplicationBootstrap from "./ui/applicationBootstrap.js";
import EngagementController from "./ui/engagementController.js";
import SimilarContentController from "./ui/similarContentController.js";
import VideoModalCommentController from "./ui/videoModalCommentController.js";
import TorrentStatusController from "./ui/torrentStatusController.js";
@@ -290,6 +291,21 @@ class Application {
getProfileCacheEntry: (pubkey) => this.getProfileCacheEntry(pubkey),
});
this.engagementController = new EngagementController({
services: {
nostrClient,
},
ui: {
showError: (msg) => this.showError(msg),
showSuccess: (msg) => this.showSuccess(msg),
showStatus: (msg, opts) => this.showStatus(msg, opts),
},
state: {
getCurrentVideo: () => this.currentVideo,
getCurrentVideoPointer: () => this.currentVideoPointer,
},
});
this.initializeModerationActionController();
this.initializeSimilarContentController();
}
@@ -7031,37 +7047,6 @@ class Application {
}
}
derivePointerFromDataset(dataset = {}, context = "") {
const type =
typeof dataset.pointerType === "string" ? dataset.pointerType.trim() : "";
const value =
typeof dataset.pointerValue === "string" ? dataset.pointerValue.trim() : "";
const relay =
typeof dataset.pointerRelay === "string" ? dataset.pointerRelay.trim() : "";
if (type && value) {
return relay ? [type, value, relay] : [type, value];
}
if (
context === "modal" &&
Array.isArray(this.currentVideoPointer) &&
this.currentVideoPointer.length >= 2
) {
return this.currentVideoPointer;
}
if (
context === "modal" &&
Array.isArray(this.currentVideo?.pointer) &&
this.currentVideo.pointer.length >= 2
) {
return this.currentVideo.pointer;
}
return null;
}
async handleMoreMenuAction(action, dataset = {}) {
if (!this.moreMenuController) {
return;
@@ -7071,332 +7056,20 @@ class Application {
}
async handleRepostAction(dataset = {}) {
const context = typeof dataset.context === "string" ? dataset.context : "";
const explicitEventId =
typeof dataset.eventId === "string" && dataset.eventId.trim()
? dataset.eventId.trim()
: "";
const fallbackEventId =
context === "modal" && this.currentVideo?.id ? this.currentVideo.id : "";
const targetEventId = explicitEventId || fallbackEventId;
if (!targetEventId) {
this.showError("No event is available to repost.");
return;
}
const pointer = this.derivePointerFromDataset(dataset, context);
let author = typeof dataset.author === "string" ? dataset.author.trim() : "";
if (!author && context === "modal" && this.currentVideo?.pubkey) {
author = this.currentVideo.pubkey;
}
const rawKindValue = (() => {
if (typeof dataset.kind === "string" && dataset.kind.trim()) {
const parsed = Number.parseInt(dataset.kind.trim(), 10);
return Number.isFinite(parsed) ? parsed : null;
}
if (Number.isFinite(dataset.kind)) {
return Number(dataset.kind);
}
if (Number.isFinite(this.currentVideo?.kind)) {
return Number(this.currentVideo.kind);
}
return null;
})();
const options = {
pointer,
pointerType: dataset.pointerType,
pointerValue: dataset.pointerValue,
pointerRelay: dataset.pointerRelay,
authorPubkey: author,
};
if (Number.isFinite(rawKindValue)) {
options.kind = Math.floor(rawKindValue);
}
try {
const result = await nostrClient.repostEvent(targetEventId, options);
if (!result?.ok) {
const code = result?.error || "repost-failed";
switch (code) {
case "invalid-event-id":
this.showError("No event is available to repost.");
break;
case "missing-actor":
this.showError(
"Cannot sign the repost right now. Please refresh and try again.",
);
break;
case "pool-unavailable":
this.showError("Cannot reach relays right now. Please try again later.");
break;
case "publish-rejected":
this.showError("No relay accepted the repost attempt.");
break;
case "signing-failed":
this.showError("Failed to sign the repost. Please try again.");
break;
default:
this.showError("Failed to repost the video. Please try again later.");
break;
}
return;
}
const acceptedCount = Array.isArray(result.summary?.accepted)
? result.summary.accepted.length
: 0;
const relayCount =
acceptedCount > 0
? acceptedCount
: Array.isArray(result.relays)
? result.relays.length
: acceptedCount;
const fragments = [];
if (relayCount > 0) {
fragments.push(
`Reposted to ${relayCount} relay${relayCount === 1 ? "" : "s"}.`,
);
} else {
fragments.push("Reposted.");
}
if (result.sessionActor) {
fragments.push("Boost as session user.");
}
this.showSuccess(fragments.join(" ").trim());
} catch (error) {
devLogger.warn("[app] Repost action failed:", error);
this.showError("Failed to repost the video. Please try again later.");
if (this.engagementController) {
return this.engagementController.handleRepostAction(dataset);
}
}
async handleMirrorAction(dataset = {}) {
const context = typeof dataset.context === "string" ? dataset.context : "";
const explicitEventId =
typeof dataset.eventId === "string" && dataset.eventId.trim()
? dataset.eventId.trim()
: "";
const fallbackEventId =
context === "modal" && this.currentVideo?.id ? this.currentVideo.id : "";
const targetEventId = explicitEventId || fallbackEventId;
if (!targetEventId) {
this.showError("No event is available to mirror.");
return;
}
const explicitUrl =
typeof dataset.url === "string" && dataset.url.trim() ? dataset.url.trim() : "";
const fallbackUrl =
context === "modal" && typeof this.currentVideo?.url === "string"
? this.currentVideo.url.trim()
: "";
const targetUrl = explicitUrl || fallbackUrl;
if (!targetUrl) {
this.showError("This video does not expose a hosted URL to mirror.");
return;
}
const rawMagnet =
typeof dataset.magnet === "string" && dataset.magnet.trim()
? dataset.magnet.trim()
: "";
const fallbackMagnet =
context === "modal"
? (this.currentVideo?.magnet || this.currentVideo?.originalMagnet || "")
: "";
const magnet = rawMagnet || fallbackMagnet;
const thumbnail =
typeof dataset.thumbnail === "string" && dataset.thumbnail.trim()
? dataset.thumbnail.trim()
: context === "modal" && typeof this.currentVideo?.thumbnail === "string"
? this.currentVideo.thumbnail.trim()
: "";
const description =
typeof dataset.description === "string" && dataset.description.trim()
? dataset.description.trim()
: context === "modal" && typeof this.currentVideo?.description === "string"
? this.currentVideo.description.trim()
: "";
const title =
typeof dataset.title === "string" && dataset.title.trim()
? dataset.title.trim()
: context === "modal" && typeof this.currentVideo?.title === "string"
? this.currentVideo.title.trim()
: "";
const datasetPrivate =
dataset.isPrivate === "true" || dataset.isPrivate === true ? true : false;
const fallbackPrivate = context === "modal" && this.currentVideo?.isPrivate === true;
const isPrivate = datasetPrivate || fallbackPrivate;
if (isPrivate) {
this.showError("Mirroring is unavailable for private videos.");
return;
}
const options = {
url: targetUrl,
magnet,
thumbnail,
description,
title,
isPrivate,
};
try {
const result = await nostrClient.mirrorVideoEvent(targetEventId, options);
if (!result?.ok) {
const code = result?.error || "mirror-failed";
switch (code) {
case "invalid-event-id":
this.showError("No event is available to mirror.");
break;
case "missing-url":
this.showError("This video does not expose a hosted URL to mirror.");
break;
case "missing-actor":
this.showError(
"Cannot sign the mirror right now. Please refresh and try again.",
);
break;
case "pool-unavailable":
this.showError("Cannot reach relays right now. Please try again later.");
break;
case "publish-rejected":
this.showError("No relay accepted the mirror attempt.");
break;
case "signing-failed":
this.showError("Failed to sign the mirror. Please try again.");
break;
default:
this.showError("Failed to mirror the video. Please try again later.");
break;
}
return;
}
const acceptedCount = Array.isArray(result.summary?.accepted)
? result.summary.accepted.length
: 0;
const relayCount =
acceptedCount > 0
? acceptedCount
: Array.isArray(result.relays)
? result.relays.length
: acceptedCount;
const fragments = [];
if (relayCount > 0) {
fragments.push(
`Mirrored to ${relayCount} relay${relayCount === 1 ? "" : "s"}.`,
);
} else {
fragments.push("Mirrored.");
}
if (result.sessionActor) {
fragments.push("Boost as session user.");
}
this.showSuccess(fragments.join(" ").trim());
} catch (error) {
devLogger.warn("[app] Mirror action failed:", error);
this.showError("Failed to mirror the video. Please try again later.");
if (this.engagementController) {
return this.engagementController.handleMirrorAction(dataset);
}
}
async handleEnsurePresenceAction(dataset = {}) {
const context = typeof dataset.context === "string" ? dataset.context : "";
const explicitEventId =
typeof dataset.eventId === "string" && dataset.eventId.trim()
? dataset.eventId.trim()
: "";
const fallbackEventId =
context === "modal" && this.currentVideo?.id ? this.currentVideo.id : "";
const targetEventId = explicitEventId || fallbackEventId;
if (!targetEventId) {
this.showError("No event is available to rebroadcast.");
return;
}
const explicitPubkey =
typeof dataset.pubkey === "string" && dataset.pubkey.trim()
? dataset.pubkey.trim()
: "";
const datasetAuthor =
typeof dataset.author === "string" && dataset.author.trim()
? dataset.author.trim()
: "";
const fallbackPubkey =
context === "modal" && typeof this.currentVideo?.pubkey === "string"
? this.currentVideo.pubkey
: datasetAuthor;
const targetPubkey = explicitPubkey || fallbackPubkey || "";
try {
const result = await nostrClient.rebroadcastEvent(targetEventId, {
pubkey: targetPubkey,
});
if (result?.throttled) {
const remainingMs = Math.max(0, Number(result?.cooldown?.remainingMs) || 0);
const remainingSeconds = Math.ceil(remainingMs / 1000);
const message =
remainingSeconds > 0
? `Rebroadcast is cooling down. Try again in ${remainingSeconds}s.`
: "Rebroadcast is cooling down. Try again soon.";
this.showStatus(message);
if (typeof window !== "undefined" && typeof window.setTimeout === "function") {
window.setTimeout(() => {
this.showStatus("");
}, 5000);
}
return;
}
if (!result?.ok) {
const code = result?.error || "rebroadcast-failed";
switch (code) {
case "event-not-found":
this.showError("Original event payload is unavailable. Reload and try again.");
break;
case "publish-rejected":
this.showError("No relay accepted the rebroadcast attempt.");
break;
case "pool-unavailable":
this.showError("Cannot reach relays right now. Please try again later.");
break;
default:
this.showError("Failed to rebroadcast. Please try again later.");
break;
}
return;
}
if (result?.alreadyPresent) {
this.showSuccess("Relays already have this revision.");
return;
}
this.showSuccess("Rebroadcast requested across relays.");
} catch (error) {
devLogger.warn("[app] Rebroadcast action failed:", error);
this.showError("Failed to rebroadcast. Please try again later.");
if (this.engagementController) {
return this.engagementController.handleEnsurePresenceAction(dataset);
}
}
+426
View File
@@ -0,0 +1,426 @@
import { devLogger } from "../utils/logger.js";
export default class EngagementController {
constructor({ services = {}, ui = {}, state = {} } = {}) {
this.services = {
nostrClient: services.nostrClient,
};
this.ui = {
showError: ui.showError || (() => {}),
showSuccess: ui.showSuccess || (() => {}),
showStatus: ui.showStatus || (() => {}),
};
this.state = {
getCurrentVideo: state.getCurrentVideo || (() => null),
getCurrentVideoPointer: state.getCurrentVideoPointer || (() => null),
};
}
derivePointerFromDataset(dataset = {}, context = "") {
const type =
typeof dataset.pointerType === "string" ? dataset.pointerType.trim() : "";
const value =
typeof dataset.pointerValue === "string"
? dataset.pointerValue.trim()
: "";
const relay =
typeof dataset.pointerRelay === "string"
? dataset.pointerRelay.trim()
: "";
if (type && value) {
return relay ? [type, value, relay] : [type, value];
}
const currentVideoPointer = this.state.getCurrentVideoPointer();
if (
context === "modal" &&
Array.isArray(currentVideoPointer) &&
currentVideoPointer.length >= 2
) {
return currentVideoPointer;
}
const currentVideo = this.state.getCurrentVideo();
if (
context === "modal" &&
Array.isArray(currentVideo?.pointer) &&
currentVideo.pointer.length >= 2
) {
return currentVideo.pointer;
}
return null;
}
async handleRepostAction(dataset = {}) {
const context = typeof dataset.context === "string" ? dataset.context : "";
const explicitEventId =
typeof dataset.eventId === "string" && dataset.eventId.trim()
? dataset.eventId.trim()
: "";
const currentVideo = this.state.getCurrentVideo();
const fallbackEventId =
context === "modal" && currentVideo?.id ? currentVideo.id : "";
const targetEventId = explicitEventId || fallbackEventId;
if (!targetEventId) {
this.ui.showError("No event is available to repost.");
return;
}
const pointer = this.derivePointerFromDataset(dataset, context);
let author =
typeof dataset.author === "string" ? dataset.author.trim() : "";
if (!author && context === "modal" && currentVideo?.pubkey) {
author = currentVideo.pubkey;
}
const rawKindValue = (() => {
if (typeof dataset.kind === "string" && dataset.kind.trim()) {
const parsed = Number.parseInt(dataset.kind.trim(), 10);
return Number.isFinite(parsed) ? parsed : null;
}
if (Number.isFinite(dataset.kind)) {
return Number(dataset.kind);
}
if (Number.isFinite(currentVideo?.kind)) {
return Number(currentVideo.kind);
}
return null;
})();
const options = {
pointer,
pointerType: dataset.pointerType,
pointerValue: dataset.pointerValue,
pointerRelay: dataset.pointerRelay,
authorPubkey: author,
};
if (Number.isFinite(rawKindValue)) {
options.kind = Math.floor(rawKindValue);
}
try {
const result = await this.services.nostrClient.repostEvent(
targetEventId,
options
);
if (!result?.ok) {
const code = result?.error || "repost-failed";
switch (code) {
case "invalid-event-id":
this.ui.showError("No event is available to repost.");
break;
case "missing-actor":
this.ui.showError(
"Cannot sign the repost right now. Please refresh and try again."
);
break;
case "pool-unavailable":
this.ui.showError(
"Cannot reach relays right now. Please try again later."
);
break;
case "publish-rejected":
this.ui.showError("No relay accepted the repost attempt.");
break;
case "signing-failed":
this.ui.showError("Failed to sign the repost. Please try again.");
break;
default:
this.ui.showError(
"Failed to repost the video. Please try again later."
);
break;
}
return;
}
const acceptedCount = Array.isArray(result.summary?.accepted)
? result.summary.accepted.length
: 0;
const relayCount =
acceptedCount > 0
? acceptedCount
: Array.isArray(result.relays)
? result.relays.length
: acceptedCount;
const fragments = [];
if (relayCount > 0) {
fragments.push(
`Reposted to ${relayCount} relay${relayCount === 1 ? "" : "s"}.`
);
} else {
fragments.push("Reposted.");
}
if (result.sessionActor) {
fragments.push("Boost as session user.");
}
this.ui.showSuccess(fragments.join(" ").trim());
} catch (error) {
devLogger.warn("[EngagementController] Repost action failed:", error);
this.ui.showError("Failed to repost the video. Please try again later.");
}
}
async handleMirrorAction(dataset = {}) {
const context = typeof dataset.context === "string" ? dataset.context : "";
const explicitEventId =
typeof dataset.eventId === "string" && dataset.eventId.trim()
? dataset.eventId.trim()
: "";
const currentVideo = this.state.getCurrentVideo();
const fallbackEventId =
context === "modal" && currentVideo?.id ? currentVideo.id : "";
const targetEventId = explicitEventId || fallbackEventId;
if (!targetEventId) {
this.ui.showError("No event is available to mirror.");
return;
}
const explicitUrl =
typeof dataset.url === "string" && dataset.url.trim()
? dataset.url.trim()
: "";
const fallbackUrl =
context === "modal" && typeof currentVideo?.url === "string"
? currentVideo.url.trim()
: "";
const targetUrl = explicitUrl || fallbackUrl;
if (!targetUrl) {
this.ui.showError("This video does not expose a hosted URL to mirror.");
return;
}
const rawMagnet =
typeof dataset.magnet === "string" && dataset.magnet.trim()
? dataset.magnet.trim()
: "";
const fallbackMagnet =
context === "modal"
? currentVideo?.magnet || currentVideo?.originalMagnet || ""
: "";
const magnet = rawMagnet || fallbackMagnet;
const thumbnail =
typeof dataset.thumbnail === "string" && dataset.thumbnail.trim()
? dataset.thumbnail.trim()
: context === "modal" && typeof currentVideo?.thumbnail === "string"
? currentVideo.thumbnail.trim()
: "";
const description =
typeof dataset.description === "string" && dataset.description.trim()
? dataset.description.trim()
: context === "modal" && typeof currentVideo?.description === "string"
? currentVideo.description.trim()
: "";
const title =
typeof dataset.title === "string" && dataset.title.trim()
? dataset.title.trim()
: context === "modal" && typeof currentVideo?.title === "string"
? currentVideo.title.trim()
: "";
const datasetPrivate =
dataset.isPrivate === "true" || dataset.isPrivate === true ? true : false;
const fallbackPrivate =
context === "modal" && currentVideo?.isPrivate === true;
const isPrivate = datasetPrivate || fallbackPrivate;
if (isPrivate) {
this.ui.showError("Mirroring is unavailable for private videos.");
return;
}
const options = {
url: targetUrl,
magnet,
thumbnail,
description,
title,
isPrivate,
};
try {
const result = await this.services.nostrClient.mirrorVideoEvent(
targetEventId,
options
);
if (!result?.ok) {
const code = result?.error || "mirror-failed";
switch (code) {
case "invalid-event-id":
this.ui.showError("No event is available to mirror.");
break;
case "missing-url":
this.ui.showError(
"This video does not expose a hosted URL to mirror."
);
break;
case "missing-actor":
this.ui.showError(
"Cannot sign the mirror right now. Please refresh and try again."
);
break;
case "pool-unavailable":
this.ui.showError(
"Cannot reach relays right now. Please try again later."
);
break;
case "publish-rejected":
this.ui.showError("No relay accepted the mirror attempt.");
break;
case "signing-failed":
this.ui.showError("Failed to sign the mirror. Please try again.");
break;
default:
this.ui.showError(
"Failed to mirror the video. Please try again later."
);
break;
}
return;
}
const acceptedCount = Array.isArray(result.summary?.accepted)
? result.summary.accepted.length
: 0;
const relayCount =
acceptedCount > 0
? acceptedCount
: Array.isArray(result.relays)
? result.relays.length
: acceptedCount;
const fragments = [];
if (relayCount > 0) {
fragments.push(
`Mirrored to ${relayCount} relay${relayCount === 1 ? "" : "s"}.`
);
} else {
fragments.push("Mirrored.");
}
if (result.sessionActor) {
fragments.push("Boost as session user.");
}
this.ui.showSuccess(fragments.join(" ").trim());
} catch (error) {
devLogger.warn("[EngagementController] Mirror action failed:", error);
this.ui.showError("Failed to mirror the video. Please try again later.");
}
}
async handleEnsurePresenceAction(dataset = {}) {
const context = typeof dataset.context === "string" ? dataset.context : "";
const explicitEventId =
typeof dataset.eventId === "string" && dataset.eventId.trim()
? dataset.eventId.trim()
: "";
const currentVideo = this.state.getCurrentVideo();
const fallbackEventId =
context === "modal" && currentVideo?.id ? currentVideo.id : "";
const targetEventId = explicitEventId || fallbackEventId;
if (!targetEventId) {
this.ui.showError("No event is available to rebroadcast.");
return;
}
const explicitPubkey =
typeof dataset.pubkey === "string" && dataset.pubkey.trim()
? dataset.pubkey.trim()
: "";
const datasetAuthor =
typeof dataset.author === "string" && dataset.author.trim()
? dataset.author.trim()
: "";
const fallbackPubkey =
context === "modal" && typeof currentVideo?.pubkey === "string"
? currentVideo.pubkey
: datasetAuthor;
const targetPubkey = explicitPubkey || fallbackPubkey || "";
try {
const result = await this.services.nostrClient.rebroadcastEvent(
targetEventId,
{
pubkey: targetPubkey,
}
);
if (result?.throttled) {
const remainingMs = Math.max(
0,
Number(result?.cooldown?.remainingMs) || 0
);
const remainingSeconds = Math.ceil(remainingMs / 1000);
const message =
remainingSeconds > 0
? `Rebroadcast is cooling down. Try again in ${remainingSeconds}s.`
: "Rebroadcast is cooling down. Try again soon.";
this.ui.showStatus(message);
if (
typeof window !== "undefined" &&
typeof window.setTimeout === "function"
) {
window.setTimeout(() => {
this.ui.showStatus("");
}, 5000);
}
return;
}
if (!result?.ok) {
const code = result?.error || "rebroadcast-failed";
switch (code) {
case "event-not-found":
this.ui.showError(
"Original event payload is unavailable. Reload and try again."
);
break;
case "publish-rejected":
this.ui.showError("No relay accepted the rebroadcast attempt.");
break;
case "pool-unavailable":
this.ui.showError(
"Cannot reach relays right now. Please try again later."
);
break;
default:
this.ui.showError(
"Failed to rebroadcast. Please try again later."
);
break;
}
return;
}
if (result?.alreadyPresent) {
this.ui.showSuccess("Relays already have this revision.");
return;
}
this.ui.showSuccess("Rebroadcast requested across relays.");
} catch (error) {
devLogger.warn("[EngagementController] Rebroadcast action failed:", error);
this.ui.showError("Failed to rebroadcast. Please try again later.");
}
}
}
+149
View File
@@ -0,0 +1,149 @@
import test from "node:test";
import assert from "node:assert/strict";
import EngagementController from "../../js/ui/engagementController.js";
test("EngagementController", async (t) => {
let controller;
let mockNostrClient;
let mockShowError;
let mockShowSuccess;
let mockShowStatus;
let mockGetCurrentVideo;
let mockGetCurrentVideoPointer;
t.beforeEach(() => {
mockNostrClient = {
repostEvent: t.mock.fn(),
mirrorVideoEvent: t.mock.fn(),
rebroadcastEvent: t.mock.fn(),
};
mockShowError = t.mock.fn();
mockShowSuccess = t.mock.fn();
mockShowStatus = t.mock.fn();
mockGetCurrentVideo = t.mock.fn();
mockGetCurrentVideoPointer = t.mock.fn();
controller = new EngagementController({
services: { nostrClient: mockNostrClient },
ui: {
showError: mockShowError,
showSuccess: mockShowSuccess,
showStatus: mockShowStatus,
},
state: {
getCurrentVideo: mockGetCurrentVideo,
getCurrentVideoPointer: mockGetCurrentVideoPointer
},
});
});
await t.test("handleRepostAction - should show error if no event ID is available", async () => {
await controller.handleRepostAction({});
assert.equal(mockShowError.mock.calls.length, 1);
assert.deepEqual(mockShowError.mock.calls[0].arguments, ["No event is available to repost."]);
assert.equal(mockNostrClient.repostEvent.mock.calls.length, 0);
});
await t.test("handleRepostAction - should call repostEvent with correct parameters", async () => {
const eventId = "test-event-id";
mockNostrClient.repostEvent.mock.mockImplementation(async () => ({ ok: true, summary: { accepted: [] } }));
await controller.handleRepostAction({ eventId, kind: "30078" });
assert.equal(mockNostrClient.repostEvent.mock.calls.length, 1);
const args = mockNostrClient.repostEvent.mock.calls[0].arguments;
assert.equal(args[0], eventId);
assert.equal(args[1].kind, 30078);
assert.equal(mockShowSuccess.mock.calls.length, 1);
assert.deepEqual(mockShowSuccess.mock.calls[0].arguments, ["Reposted."]);
});
await t.test("handleRepostAction - should handle failure from repostEvent", async () => {
const eventId = "test-event-id";
mockNostrClient.repostEvent.mock.mockImplementation(async () => ({ ok: false, error: "signing-failed" }));
await controller.handleRepostAction({ eventId });
assert.equal(mockShowError.mock.calls.length, 1);
assert.deepEqual(mockShowError.mock.calls[0].arguments, ["Failed to sign the repost. Please try again."]);
});
await t.test("handleRepostAction - should use currentVideoPointer when in modal context", async () => {
const eventId = "test-event-id";
const videoPointer = ["nevent", "test-nevent", "wss://relay.example.com"];
mockNostrClient.repostEvent.mock.mockImplementation(async () => ({ ok: true, summary: { accepted: [] } }));
// In node:test, mocking return values is done via implementation if it's a mock function
mockGetCurrentVideoPointer.mock.mockImplementation(() => videoPointer);
await controller.handleRepostAction({ eventId, context: "modal" });
assert.equal(mockNostrClient.repostEvent.mock.calls.length, 1);
const args = mockNostrClient.repostEvent.mock.calls[0].arguments;
assert.deepEqual(args[1].pointer, videoPointer);
});
await t.test("handleMirrorAction - should show error if video has no URL", async () => {
const eventId = "test-event-id";
await controller.handleMirrorAction({ eventId });
assert.equal(mockShowError.mock.calls.length, 1);
assert.deepEqual(mockShowError.mock.calls[0].arguments, ["This video does not expose a hosted URL to mirror."]);
});
await t.test("handleMirrorAction - should call mirrorVideoEvent when URL is provided", async () => {
const eventId = "test-event-id";
const url = "https://example.com/video.mp4";
mockNostrClient.mirrorVideoEvent.mock.mockImplementation(async () => ({ ok: true, summary: { accepted: [] } }));
await controller.handleMirrorAction({ eventId, url });
assert.equal(mockNostrClient.mirrorVideoEvent.mock.calls.length, 1);
const args = mockNostrClient.mirrorVideoEvent.mock.calls[0].arguments;
assert.equal(args[0], eventId);
assert.equal(args[1].url, url);
assert.equal(mockShowSuccess.mock.calls.length, 1);
assert.deepEqual(mockShowSuccess.mock.calls[0].arguments, ["Mirrored."]);
});
await t.test("handleMirrorAction - should prevent mirroring private videos", async () => {
const eventId = "test-event-id";
const url = "https://example.com/video.mp4";
await controller.handleMirrorAction({ eventId, url, isPrivate: true });
assert.equal(mockShowError.mock.calls.length, 1);
assert.deepEqual(mockShowError.mock.calls[0].arguments, ["Mirroring is unavailable for private videos."]);
assert.equal(mockNostrClient.mirrorVideoEvent.mock.calls.length, 0);
});
await t.test("handleEnsurePresenceAction - should show error if no event ID", async () => {
await controller.handleEnsurePresenceAction({});
assert.equal(mockShowError.mock.calls.length, 1);
assert.deepEqual(mockShowError.mock.calls[0].arguments, ["No event is available to rebroadcast."]);
});
await t.test("handleEnsurePresenceAction - should handle throttled response", async () => {
const eventId = "test-event-id";
mockNostrClient.rebroadcastEvent.mock.mockImplementation(async () => ({ throttled: true, cooldown: { remainingMs: 5000 } }));
global.window = { setTimeout: t.mock.fn() };
await controller.handleEnsurePresenceAction({ eventId });
assert.equal(mockShowStatus.mock.calls.length, 1);
assert.match(mockShowStatus.mock.calls[0].arguments[0], /Rebroadcast is cooling down/);
delete global.window;
});
await t.test("handleEnsurePresenceAction - should show success on successful rebroadcast", async () => {
const eventId = "test-event-id";
mockNostrClient.rebroadcastEvent.mock.mockImplementation(async () => ({ ok: true }));
await controller.handleEnsurePresenceAction({ eventId });
assert.equal(mockShowSuccess.mock.calls.length, 1);
assert.deepEqual(mockShowSuccess.mock.calls[0].arguments, ["Rebroadcast requested across relays."]);
});
});