mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 21:02:28 +00:00
update
This commit is contained in:
1 parent
589925f1fe
commit
ef29bb6eb3
97 files changed
+46855
No files matched your search
+370
@@ -0,0 +1,370 @@
|
||||
import { subscriptions } from "../subscriptions.js";
|
||||
import { profileCache } from "../state/profileCache.js";
|
||||
import { formatTimeAgo } from "../utils/formatters.js";
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
import { getApplication } from "../applicationContext.js";
|
||||
|
||||
export class SubscriptionHistoryController {
|
||||
constructor(options = {}) {
|
||||
this.modal = null;
|
||||
this.activePubkey = null;
|
||||
this.selectedEvent = null;
|
||||
this.historyEvents = [];
|
||||
this.initialized = false;
|
||||
this.callbacks = {
|
||||
onClose: options.onClose || (() => {}),
|
||||
};
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("components/subscription-history-modal.html");
|
||||
if (!response.ok) throw new Error("HTML not found");
|
||||
const html = await response.text();
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = html;
|
||||
|
||||
// Append to body if not already present (avoid duplicates)
|
||||
if (!document.getElementById("subscriptionHistoryModal")) {
|
||||
document.body.appendChild(template.content);
|
||||
}
|
||||
|
||||
this.modal = document.getElementById("subscriptionHistoryModal");
|
||||
this.bindElements();
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
userLogger.error("Failed to load subscription history modal:", error);
|
||||
}
|
||||
}
|
||||
|
||||
bindElements() {
|
||||
if (!this.modal) return;
|
||||
|
||||
this.listContainer = this.modal.querySelector("#subscriptionHistoryList");
|
||||
this.detailsContainer = this.modal.querySelector(
|
||||
"#subscriptionHistoryDetailsContent"
|
||||
);
|
||||
this.detailsEmpty = this.modal.querySelector(
|
||||
"#subscriptionHistoryDetailsEmpty"
|
||||
);
|
||||
this.loadingIndicator = this.modal.querySelector(
|
||||
"#subscriptionHistoryLoading"
|
||||
);
|
||||
this.emptyIndicator = this.modal.querySelector("#subscriptionHistoryEmpty");
|
||||
this.decryptingIndicator = this.modal.querySelector(
|
||||
"#subscriptionHistoryDecrypting"
|
||||
);
|
||||
this.selectedChannelsList = this.modal.querySelector(
|
||||
"#subscriptionHistorySelectedChannels"
|
||||
);
|
||||
this.selectedDate = this.modal.querySelector(
|
||||
"#subscriptionHistorySelectedDate"
|
||||
);
|
||||
this.selectedMeta = this.modal.querySelector(
|
||||
"#subscriptionHistorySelectedMeta"
|
||||
);
|
||||
this.selectedBadge = this.modal.querySelector(
|
||||
"#subscriptionHistorySelectedBadge"
|
||||
);
|
||||
|
||||
this.closeBtn = this.modal.querySelector("#closeSubscriptionHistoryModal");
|
||||
this.restoreBtn = this.modal.querySelector("#subscriptionHistoryRestoreBtn");
|
||||
this.rebroadcastBtn = this.modal.querySelector(
|
||||
"#subscriptionHistoryRebroadcastBtn"
|
||||
);
|
||||
|
||||
this.closeBtn?.addEventListener("click", () => this.hide());
|
||||
this.restoreBtn?.addEventListener("click", () => this.handleRestore());
|
||||
this.rebroadcastBtn?.addEventListener("click", () =>
|
||||
this.handleRebroadcast()
|
||||
);
|
||||
|
||||
this.modal
|
||||
.querySelector(".bv-modal-backdrop")
|
||||
?.addEventListener("click", () => this.hide());
|
||||
}
|
||||
|
||||
async show(pubkey) {
|
||||
if (!this.initialized) await this.load();
|
||||
if (!this.modal) return;
|
||||
|
||||
this.activePubkey = pubkey;
|
||||
this.modal.classList.remove("hidden");
|
||||
this.modal.setAttribute("aria-hidden", "false");
|
||||
// Ensure modal-open class is added to body for scroll locking if design system requires it
|
||||
document.body.classList.add("modal-open");
|
||||
|
||||
this.fetchHistory();
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.modal) return;
|
||||
this.modal.classList.add("hidden");
|
||||
this.modal.setAttribute("aria-hidden", "true");
|
||||
// Only remove if no other modals are open, ideally.
|
||||
// But for simplicity:
|
||||
document.body.classList.remove("modal-open");
|
||||
|
||||
this.selectedEvent = null;
|
||||
// Reset view
|
||||
this.detailsEmpty.classList.remove("hidden");
|
||||
this.detailsContainer.classList.add("hidden");
|
||||
this.callbacks.onClose();
|
||||
}
|
||||
|
||||
async fetchHistory() {
|
||||
if (!this.loadingIndicator) return;
|
||||
this.loadingIndicator.classList.remove("hidden");
|
||||
this.listContainer.replaceChildren();
|
||||
this.emptyIndicator.classList.add("hidden");
|
||||
|
||||
try {
|
||||
this.historyEvents = await subscriptions.fetchHistory(this.activePubkey);
|
||||
this.renderList();
|
||||
} catch (error) {
|
||||
userLogger.error("Failed to fetch subscription history:", error);
|
||||
this.emptyIndicator.textContent = "Failed to load history.";
|
||||
this.emptyIndicator.classList.remove("hidden");
|
||||
} finally {
|
||||
this.loadingIndicator.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
renderList() {
|
||||
this.listContainer.replaceChildren();
|
||||
if (!this.historyEvents.length) {
|
||||
this.emptyIndicator.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
this.historyEvents.forEach((event) => {
|
||||
const item = document.createElement("li");
|
||||
item.className =
|
||||
"card p-3 cursor-pointer hover:bg-surface-2 transition-colors border-l-4 border-transparent rounded-r-md";
|
||||
|
||||
const dTag = event.tags.find((t) => t[0] === "d")?.[1];
|
||||
const isBackup = dTag && dTag.startsWith("subscriptions-backup-");
|
||||
|
||||
const date = new Date(event.created_at * 1000);
|
||||
const dateStr = date.toLocaleString(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
const timeAgo = formatTimeAgo(event.created_at);
|
||||
|
||||
const divFlex = document.createElement("div");
|
||||
divFlex.className = "flex justify-between items-start";
|
||||
|
||||
const divLeft = document.createElement("div");
|
||||
|
||||
const pTitle = document.createElement("p");
|
||||
pTitle.className = "text-sm font-medium text-text";
|
||||
pTitle.textContent = isBackup ? "Backup" : "Standard List";
|
||||
|
||||
const pDate = document.createElement("p");
|
||||
pDate.className = "text-xs text-muted";
|
||||
pDate.textContent = dateStr;
|
||||
|
||||
divLeft.appendChild(pTitle);
|
||||
divLeft.appendChild(pDate);
|
||||
|
||||
const spanTime = document.createElement("span");
|
||||
spanTime.className = "text-xs text-muted whitespace-nowrap ml-2";
|
||||
spanTime.textContent = timeAgo;
|
||||
|
||||
divFlex.appendChild(divLeft);
|
||||
divFlex.appendChild(spanTime);
|
||||
|
||||
item.appendChild(divFlex);
|
||||
|
||||
item.addEventListener("click", () => {
|
||||
this.listContainer.querySelectorAll("li").forEach((li) => {
|
||||
li.classList.remove("bg-surface-2", "border-accent");
|
||||
li.classList.add("border-transparent");
|
||||
});
|
||||
item.classList.add("bg-surface-2", "border-accent");
|
||||
item.classList.remove("border-transparent");
|
||||
this.selectEvent(event);
|
||||
});
|
||||
|
||||
this.listContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async selectEvent(event) {
|
||||
this.selectedEvent = event;
|
||||
this.detailsEmpty.classList.add("hidden");
|
||||
this.detailsContainer.classList.remove("hidden");
|
||||
this.selectedChannelsList.replaceChildren();
|
||||
this.decryptingIndicator.classList.remove("hidden");
|
||||
|
||||
const date = new Date(event.created_at * 1000);
|
||||
this.selectedDate.textContent = date.toLocaleString(undefined, {
|
||||
dateStyle: "full",
|
||||
timeStyle: "short",
|
||||
});
|
||||
|
||||
const dTag = event.tags.find((t) => t[0] === "d")?.[1];
|
||||
const isBackup = dTag?.startsWith("subscriptions-backup-");
|
||||
|
||||
this.selectedBadge.textContent = isBackup ? "Backup" : "Active / Standard";
|
||||
this.selectedBadge.dataset.variant = isBackup ? "accent" : "surface";
|
||||
|
||||
try {
|
||||
const decryptResult = await subscriptions.decryptSubscriptionEvent(
|
||||
event,
|
||||
this.activePubkey
|
||||
);
|
||||
if (!decryptResult.ok) {
|
||||
throw new Error("Decryption failed");
|
||||
}
|
||||
|
||||
let pubkeys = [];
|
||||
try {
|
||||
const parsed = JSON.parse(decryptResult.plaintext);
|
||||
if (Array.isArray(parsed)) {
|
||||
// [['p', hex], ...]
|
||||
pubkeys = parsed
|
||||
.filter((t) => Array.isArray(t) && t[0] === "p")
|
||||
.map((t) => t[1]);
|
||||
}
|
||||
} catch (e) {
|
||||
userLogger.warn("Failed to parse plaintext JSON", e);
|
||||
}
|
||||
|
||||
this.selectedMeta.textContent = `${pubkeys.length} channels`;
|
||||
this.renderChannels(pubkeys);
|
||||
} catch (error) {
|
||||
userLogger.error("Failed to decrypt event:", error);
|
||||
const errorP = document.createElement("p");
|
||||
errorP.className = "text-critical text-sm text-center w-full mt-4";
|
||||
errorP.textContent = "Decryption failed or content is invalid.";
|
||||
this.selectedChannelsList.replaceChildren(errorP);
|
||||
this.selectedMeta.textContent = "Unknown content";
|
||||
} finally {
|
||||
this.decryptingIndicator.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
async renderChannels(pubkeys) {
|
||||
this.selectedChannelsList.replaceChildren();
|
||||
|
||||
const app = getApplication();
|
||||
if (app && typeof app.batchFetchProfiles === "function") {
|
||||
try {
|
||||
await app.batchFetchProfiles(pubkeys);
|
||||
} catch (e) {
|
||||
userLogger.warn("Failed to batch fetch profiles", e);
|
||||
}
|
||||
}
|
||||
|
||||
pubkeys.forEach((pubkey) => {
|
||||
const profile = profileCache.getProfile(pubkey);
|
||||
const name = profile?.name || profile?.display_name || pubkey.slice(0, 8);
|
||||
const avatar = profile?.picture || "assets/svg/default-profile.svg";
|
||||
|
||||
const item = document.createElement("li");
|
||||
item.className =
|
||||
"flex items-center gap-3 p-2 rounded-md hover:bg-surface-2 border border-transparent hover:border-border/50 transition-colors";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = avatar;
|
||||
img.loading = "lazy";
|
||||
img.decoding = "async";
|
||||
img.className = "w-8 h-8 rounded-full object-cover bg-surface-2";
|
||||
img.onerror = function () {
|
||||
this.src = "assets/svg/default-profile.svg";
|
||||
};
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "overflow-hidden";
|
||||
|
||||
const pName = document.createElement("p");
|
||||
pName.className = "text-sm font-medium truncate text-text";
|
||||
pName.textContent = name;
|
||||
|
||||
const pKey = document.createElement("p");
|
||||
pKey.className = "text-xs text-muted truncate font-mono";
|
||||
pKey.textContent = `${pubkey.slice(0, 10)}...`;
|
||||
|
||||
div.appendChild(pName);
|
||||
div.appendChild(pKey);
|
||||
|
||||
item.appendChild(img);
|
||||
item.appendChild(div);
|
||||
|
||||
this.selectedChannelsList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async handleRestore() {
|
||||
if (!this.selectedEvent) return;
|
||||
if (
|
||||
!confirm(
|
||||
"Are you sure you want to restore this list? It will overwrite your current subscriptions."
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
this.restoreBtn.disabled = true;
|
||||
this.restoreBtn.textContent = "Restoring...";
|
||||
|
||||
try {
|
||||
await subscriptions.restoreBackup(this.selectedEvent, this.activePubkey);
|
||||
// alert("Subscriptions restored successfully!"); // alert blocks, toast is better if available, but alert is fine for now
|
||||
this.hide();
|
||||
} catch (error) {
|
||||
userLogger.error("Restore failed:", error);
|
||||
alert("Failed to restore: " + error.message);
|
||||
} finally {
|
||||
this.restoreBtn.disabled = false;
|
||||
this.restoreBtn.textContent = "Restore as Active";
|
||||
}
|
||||
}
|
||||
|
||||
async handleRebroadcast() {
|
||||
if (!this.selectedEvent) return;
|
||||
this.rebroadcastBtn.disabled = true;
|
||||
this.rebroadcastBtn.textContent = "Broadcasting...";
|
||||
|
||||
try {
|
||||
await subscriptions.rebroadcastBackup(
|
||||
this.selectedEvent,
|
||||
this.activePubkey
|
||||
);
|
||||
// Refresh to show potentially new event
|
||||
this.fetchHistory();
|
||||
} catch (error) {
|
||||
userLogger.error("Rebroadcast failed:", error);
|
||||
alert("Failed to rebroadcast: " + error.message);
|
||||
} finally {
|
||||
this.rebroadcastBtn.disabled = false;
|
||||
this.rebroadcastBtn.textContent = "Rebroadcast";
|
||||
}
|
||||
}
|
||||
|
||||
async handleCreateBackup(pubkey) {
|
||||
if (!pubkey) return;
|
||||
|
||||
try {
|
||||
await subscriptions.createBackup(pubkey);
|
||||
// If modal is open, refresh
|
||||
if (
|
||||
this.modal &&
|
||||
!this.modal.classList.contains("hidden") &&
|
||||
this.activePubkey === pubkey
|
||||
) {
|
||||
this.fetchHistory();
|
||||
} else {
|
||||
// Show success feedback
|
||||
// Using a simple alert or console log if UI controller doesn't pass a toast function
|
||||
// userLogger.info("Backup created");
|
||||
}
|
||||
} catch (error) {
|
||||
userLogger.error("Backup failed:", error);
|
||||
alert("Backup failed: " + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user