Files
bitvid/js/ui/engagementController.js
T

428 lines
13 KiB
JavaScript

import { devLogger } from "../utils/logger.js";
import { SHORT_TIMEOUT_MS } from "../constants.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("");
}, SHORT_TIMEOUT_MS);
}
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.");
}
}
}