mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
372 lines
10 KiB
JavaScript
372 lines
10 KiB
JavaScript
import { isDevMode } from "../config.js";
|
|
import {
|
|
normalizePointerInput,
|
|
pointerKey as derivePointerKey,
|
|
} from "../nostr/watchHistory.js";
|
|
import { normalizeDesignSystemContext } from "../designSystem.js";
|
|
import { devLogger } from "../utils/logger.js";
|
|
|
|
const noop = () => {};
|
|
|
|
function resolveCardElement(trigger) {
|
|
if (!(trigger instanceof HTMLElement)) {
|
|
return null;
|
|
}
|
|
const candidate = trigger.closest("[data-history-card]");
|
|
return candidate instanceof HTMLElement ? candidate : null;
|
|
}
|
|
|
|
function safeInvoke(callback, payload) {
|
|
if (typeof callback !== "function") {
|
|
return;
|
|
}
|
|
try {
|
|
callback(payload);
|
|
} catch (error) {
|
|
devLogger.warn(
|
|
"[watchHistoryController] onStateChange handler failed:",
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
export default class WatchHistoryController {
|
|
constructor({
|
|
watchHistoryService,
|
|
nostrClient,
|
|
showError,
|
|
showSuccess,
|
|
dropWatchHistoryMetadata,
|
|
getActivePubkey,
|
|
designSystem,
|
|
} = {}) {
|
|
this.watchHistoryService = watchHistoryService || null;
|
|
this.nostrClient = nostrClient || null;
|
|
this.showError = typeof showError === "function" ? showError : noop;
|
|
this.showSuccess = typeof showSuccess === "function" ? showSuccess : noop;
|
|
this.dropWatchHistoryMetadata =
|
|
typeof dropWatchHistoryMetadata === "function"
|
|
? dropWatchHistoryMetadata
|
|
: noop;
|
|
this.getActivePubkey =
|
|
typeof getActivePubkey === "function" ? getActivePubkey : () => "";
|
|
this.designSystem = normalizeDesignSystemContext(designSystem);
|
|
}
|
|
|
|
buildPointerFromDataset(dataset = {}) {
|
|
if (!dataset || typeof dataset !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const typeValue =
|
|
typeof dataset.pointerType === "string" ? dataset.pointerType : "";
|
|
const normalizedType = typeValue === "a" ? "a" : typeValue === "e" ? "e" : "";
|
|
const value =
|
|
typeof dataset.pointerValue === "string" && dataset.pointerValue.trim()
|
|
? dataset.pointerValue.trim()
|
|
: "";
|
|
|
|
if (!normalizedType || !value) {
|
|
return null;
|
|
}
|
|
|
|
const pointer = { type: normalizedType, value };
|
|
|
|
if (typeof dataset.pointerRelay === "string" && dataset.pointerRelay.trim()) {
|
|
pointer.relay = dataset.pointerRelay.trim();
|
|
}
|
|
|
|
if (typeof dataset.pointerWatchedAt === "string" && dataset.pointerWatchedAt) {
|
|
const parsed = Number.parseInt(dataset.pointerWatchedAt, 10);
|
|
if (Number.isFinite(parsed)) {
|
|
pointer.watchedAt = parsed;
|
|
}
|
|
}
|
|
|
|
if (dataset.pointerSession === "true") {
|
|
pointer.session = true;
|
|
}
|
|
|
|
return pointer;
|
|
}
|
|
|
|
resolvePointerKey(pointer, explicitKey) {
|
|
if (typeof explicitKey === "string" && explicitKey.trim()) {
|
|
return explicitKey.trim();
|
|
}
|
|
|
|
const normalized = normalizePointerInput(pointer);
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
|
|
try {
|
|
return derivePointerKey(normalized) || "";
|
|
} catch (error) {
|
|
devLogger.warn(
|
|
"[watchHistoryController] Failed to derive pointer key:",
|
|
error,
|
|
);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
applyCardState(card, { status, removeCard } = {}) {
|
|
if (!(card instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
if (status === "pending") {
|
|
card.dataset.historyRemovalPending = "true";
|
|
card.classList.add("opacity-60");
|
|
card.classList.add("pointer-events-none");
|
|
return;
|
|
}
|
|
|
|
delete card.dataset.historyRemovalPending;
|
|
card.classList.remove("opacity-60", "pointer-events-none");
|
|
|
|
if (status === "removed" && removeCard) {
|
|
card.remove();
|
|
}
|
|
}
|
|
|
|
resolveActor(actor) {
|
|
if (typeof actor === "string" && actor.trim()) {
|
|
return actor.trim();
|
|
}
|
|
|
|
const candidate = this.getActivePubkey();
|
|
if (typeof candidate === "string" && candidate.trim()) {
|
|
return candidate.trim();
|
|
}
|
|
|
|
const sessionPubkey = this.nostrClient?.sessionActor?.pubkey;
|
|
if (typeof sessionPubkey === "string" && sessionPubkey.trim()) {
|
|
return sessionPubkey.trim();
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
async removeEntry(options = {}) {
|
|
if (!this.watchHistoryService?.isEnabled?.()) {
|
|
this.showError("Watch history sync is not available right now.");
|
|
const error = new Error("watch-history-disabled");
|
|
error.handled = true;
|
|
throw error;
|
|
}
|
|
|
|
const {
|
|
dataset = {},
|
|
pointer: pointerInput = null,
|
|
pointerKey: explicitPointerKey = "",
|
|
reason: providedReason,
|
|
trigger = null,
|
|
removeCard = false,
|
|
actor,
|
|
items,
|
|
onStateChange,
|
|
} = options;
|
|
|
|
const pointer = pointerInput || this.buildPointerFromDataset(dataset);
|
|
const pointerKey = this.resolvePointerKey(pointer, explicitPointerKey || dataset.pointerKey);
|
|
|
|
if (!pointerKey) {
|
|
this.showError("Unable to determine which history entry to remove.");
|
|
return null;
|
|
}
|
|
|
|
const reason =
|
|
typeof providedReason === "string" && providedReason.trim()
|
|
? providedReason.trim()
|
|
: typeof dataset.reason === "string" && dataset.reason.trim()
|
|
? dataset.reason.trim()
|
|
: "remove-item";
|
|
|
|
const card = resolveCardElement(trigger);
|
|
this.applyCardState(card, { status: "pending" });
|
|
safeInvoke(onStateChange, { status: "pending", card, removeCard });
|
|
|
|
try {
|
|
const result = await this.handleWatchHistoryRemoval({
|
|
actor,
|
|
items,
|
|
reason,
|
|
removed: {
|
|
pointer,
|
|
pointerKey,
|
|
},
|
|
});
|
|
|
|
this.applyCardState(card, { status: removeCard ? "removed" : "idle", removeCard });
|
|
safeInvoke(onStateChange, {
|
|
status: removeCard ? "removed" : "idle",
|
|
card,
|
|
removeCard,
|
|
result,
|
|
});
|
|
|
|
return result;
|
|
} catch (error) {
|
|
this.applyCardState(card, { status: "idle" });
|
|
safeInvoke(onStateChange, { status: "error", card, error, removeCard });
|
|
|
|
if (!error?.handled) {
|
|
this.showError("Failed to remove from history. Please try again.");
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async handleWatchHistoryRemoval(payload = {}) {
|
|
if (!this.watchHistoryService?.isEnabled?.()) {
|
|
const error = new Error("watch-history-disabled");
|
|
error.handled = true;
|
|
this.showError("Watch history sync is not available right now.");
|
|
throw error;
|
|
}
|
|
|
|
const reason =
|
|
typeof payload.reason === "string" && payload.reason.trim()
|
|
? payload.reason.trim()
|
|
: "remove-item";
|
|
|
|
const actorCandidate = this.resolveActor(payload.actor);
|
|
|
|
const removedPointerRaw =
|
|
payload?.removed?.pointer || payload?.removed?.raw || payload?.removed || null;
|
|
const removedPointerNormalized = normalizePointerInput(removedPointerRaw);
|
|
let removedPointerKey =
|
|
typeof payload?.removed?.pointerKey === "string"
|
|
? payload.removed.pointerKey
|
|
: "";
|
|
if (!removedPointerKey && removedPointerNormalized) {
|
|
removedPointerKey = derivePointerKey(removedPointerNormalized) || "";
|
|
}
|
|
|
|
if (removedPointerKey) {
|
|
this.dropWatchHistoryMetadata(removedPointerKey);
|
|
}
|
|
|
|
const normalizeEntry = (entry) => {
|
|
if (!entry) {
|
|
return null;
|
|
}
|
|
const pointer = normalizePointerInput(entry.pointer || entry);
|
|
if (!pointer) {
|
|
return null;
|
|
}
|
|
if (Number.isFinite(entry.watchedAt)) {
|
|
pointer.watchedAt = Math.floor(entry.watchedAt);
|
|
} else if (Number.isFinite(entry.pointer?.watchedAt)) {
|
|
pointer.watchedAt = Math.floor(entry.pointer.watchedAt);
|
|
}
|
|
if (entry.pointer?.session === true || entry.session === true) {
|
|
pointer.session = true;
|
|
}
|
|
return pointer;
|
|
};
|
|
|
|
let normalizedItems = null;
|
|
|
|
if (Array.isArray(payload.items) && payload.items.length) {
|
|
normalizedItems = payload.items.map(normalizeEntry).filter(Boolean);
|
|
}
|
|
|
|
if (!normalizedItems) {
|
|
try {
|
|
const latest = await this.watchHistoryService.loadLatest(
|
|
actorCandidate,
|
|
{ allowStale: false },
|
|
);
|
|
normalizedItems = Array.isArray(latest)
|
|
? latest.map(normalizeEntry).filter(Boolean)
|
|
: [];
|
|
} catch (error) {
|
|
const message =
|
|
error?.code === "watch-history-extension-permission-denied"
|
|
? error.message
|
|
: "Failed to load watch history. Please try again.";
|
|
this.showError(message);
|
|
if (error && typeof error === "object") {
|
|
error.handled = true;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (removedPointerKey) {
|
|
normalizedItems = normalizedItems.filter((entry) => {
|
|
try {
|
|
return derivePointerKey(entry) !== removedPointerKey;
|
|
} catch (error) {
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
this.showSuccess("Removing from history…");
|
|
|
|
try {
|
|
const snapshotResult = await this.watchHistoryService.snapshot(normalizedItems, {
|
|
actor: actorCandidate || undefined,
|
|
reason,
|
|
});
|
|
|
|
try {
|
|
await this.nostrClient?.updateWatchHistoryList?.(normalizedItems, {
|
|
actorPubkey: actorCandidate || undefined,
|
|
replace: true,
|
|
source: reason,
|
|
});
|
|
} catch (updateError) {
|
|
devLogger.warn(
|
|
"[watchHistoryController] Failed to update local watch history list:",
|
|
updateError,
|
|
);
|
|
}
|
|
|
|
this.showSuccess(
|
|
"Removed from encrypted history. Relay sync may take a moment.",
|
|
);
|
|
|
|
return { handledToasts: true, snapshot: snapshotResult };
|
|
} catch (error) {
|
|
let message = "Failed to remove from history. Please try again.";
|
|
if (error?.result?.retryable) {
|
|
message =
|
|
"Removal will retry once encrypted history is accepted by your relays.";
|
|
}
|
|
this.showError(message);
|
|
if (error && typeof error === "object") {
|
|
error.handled = true;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
flush(reason = "session-end", context = "watch-history") {
|
|
if (!this.watchHistoryService?.isEnabled?.()) {
|
|
return Promise.resolve();
|
|
}
|
|
try {
|
|
const result = this.watchHistoryService.snapshot(undefined, { reason });
|
|
return Promise.resolve(result).catch((error) => {
|
|
devLogger.warn(
|
|
`[${context}] Watch history flush failed:`,
|
|
error,
|
|
);
|
|
throw error;
|
|
});
|
|
} catch (error) {
|
|
devLogger.warn(
|
|
`[${context}] Failed to queue watch history flush:`,
|
|
error,
|
|
);
|
|
return Promise.reject(error);
|
|
}
|
|
}
|
|
}
|