mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 17:32:26 +00:00
Guard user block list from self entries
This commit is contained in:
1 parent
511a016db4
commit
62cbc2a2eb
4 files changed
+658
-28
No files matched your search
@@ -177,6 +177,30 @@
|
||||
role="tabpanel"
|
||||
aria-labelledby="profileNavBlocked"
|
||||
>
|
||||
<p class="text-sm text-gray-400">
|
||||
Creators you block are hidden from your BitVid experience. This
|
||||
list is private to your account.
|
||||
</p>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<label class="flex-1 text-sm text-gray-300" for="blockedInput">
|
||||
<span class="mb-2 block font-medium">Creator npub</span>
|
||||
<input
|
||||
id="blockedInput"
|
||||
type="text"
|
||||
inputmode="text"
|
||||
autocomplete="off"
|
||||
class="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="npub1..."
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
id="addBlockedBtn"
|
||||
type="button"
|
||||
class="px-4 py-2 rounded-md bg-blue-600 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
|
||||
>
|
||||
Block creator
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="blockedEmpty"
|
||||
class="rounded-lg border border-dashed border-gray-700 p-6 text-center text-sm text-gray-400"
|
||||
|
||||
@@ -13,6 +13,7 @@ import { trackVideoView } from "./analytics.js";
|
||||
import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
import { ADMIN_INITIAL_EVENT_BLACKLIST } from "./lists.js";
|
||||
import { userBlocks } from "./userBlocks.js";
|
||||
import {
|
||||
loadR2Settings,
|
||||
saveR2Settings,
|
||||
@@ -513,6 +514,8 @@ class bitvidApp {
|
||||
this.profileRelayList = null;
|
||||
this.profileBlockedList = null;
|
||||
this.profileBlockedEmpty = null;
|
||||
this.profileBlockedInput = null;
|
||||
this.profileAddBlockedBtn = null;
|
||||
this.profileRelayInput = null;
|
||||
this.profileAddRelayBtn = null;
|
||||
this.profileRestoreRelaysBtn = null;
|
||||
@@ -913,7 +916,11 @@ class bitvidApp {
|
||||
const savedPubKey = localStorage.getItem("userPubKey");
|
||||
if (savedPubKey) {
|
||||
// Auto-login if a pubkey was saved
|
||||
this.login(savedPubKey, false);
|
||||
try {
|
||||
await this.login(savedPubKey, false);
|
||||
} catch (error) {
|
||||
console.error("Auto-login failed:", error);
|
||||
}
|
||||
|
||||
// If the user was already logged in, show the Subscriptions link
|
||||
if (this.subscriptionsLink) {
|
||||
@@ -3299,6 +3306,10 @@ class bitvidApp {
|
||||
this.profileBlockedList = document.getElementById("blockedList") || null;
|
||||
this.profileBlockedEmpty =
|
||||
document.getElementById("blockedEmpty") || null;
|
||||
this.profileBlockedInput =
|
||||
document.getElementById("blockedInput") || null;
|
||||
this.profileAddBlockedBtn =
|
||||
document.getElementById("addBlockedBtn") || null;
|
||||
this.profileRelayInput = document.getElementById("relayInput") || null;
|
||||
this.profileAddRelayBtn = document.getElementById("addRelayBtn") || null;
|
||||
this.profileRestoreRelaysBtn =
|
||||
@@ -3388,6 +3399,28 @@ class bitvidApp {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.profileAddBlockedBtn &&
|
||||
this.profileAddBlockedBtn.dataset.bound !== "true"
|
||||
) {
|
||||
this.profileAddBlockedBtn.dataset.bound = "true";
|
||||
this.profileAddBlockedBtn.addEventListener("click", () => {
|
||||
this.handleAddBlockedCreator();
|
||||
});
|
||||
}
|
||||
if (
|
||||
this.profileBlockedInput &&
|
||||
this.profileBlockedInput.dataset.bound !== "true"
|
||||
) {
|
||||
this.profileBlockedInput.dataset.bound = "true";
|
||||
this.profileBlockedInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.handleAddBlockedCreator();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.adminAddModeratorBtn &&
|
||||
this.adminAddModeratorBtn.dataset.bound !== "true"
|
||||
@@ -3554,6 +3587,11 @@ class bitvidApp {
|
||||
|
||||
this.selectProfilePane("account");
|
||||
this.populateProfileRelays();
|
||||
try {
|
||||
await userBlocks.ensureLoaded(this.pubkey);
|
||||
} catch (error) {
|
||||
console.warn("Failed to refresh user block list while opening profile modal:", error);
|
||||
}
|
||||
this.populateBlockedList();
|
||||
|
||||
this.profileModal.classList.remove("hidden");
|
||||
@@ -3755,15 +3793,88 @@ class bitvidApp {
|
||||
});
|
||||
}
|
||||
|
||||
populateBlockedList(blocked = []) {
|
||||
populateBlockedList(blocked = null) {
|
||||
if (!this.profileBlockedList || !this.profileBlockedEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = Array.isArray(blocked) ? blocked : [];
|
||||
const sourceEntries =
|
||||
Array.isArray(blocked) && blocked.length
|
||||
? blocked
|
||||
: userBlocks.getBlockedPubkeys();
|
||||
|
||||
const normalizedEntries = [];
|
||||
const pushEntry = (hex, label) => {
|
||||
if (!hex || !label) {
|
||||
return;
|
||||
}
|
||||
normalizedEntries.push({ hex, label });
|
||||
};
|
||||
|
||||
sourceEntries.forEach((entry) => {
|
||||
if (typeof entry === "string") {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
const decoded = this.safeDecodeNpub(trimmed);
|
||||
if (!decoded) {
|
||||
return;
|
||||
}
|
||||
const label = this.safeEncodeNpub(decoded) || trimmed;
|
||||
pushEntry(decoded, label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
const hex = trimmed.toLowerCase();
|
||||
const label = this.safeEncodeNpub(hex) || hex;
|
||||
pushEntry(hex, label);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry && typeof entry === "object") {
|
||||
const candidateNpub =
|
||||
typeof entry.npub === "string" ? entry.npub.trim() : "";
|
||||
const candidateHex =
|
||||
typeof entry.pubkey === "string" ? entry.pubkey.trim() : "";
|
||||
|
||||
if (candidateHex && /^[0-9a-f]{64}$/i.test(candidateHex)) {
|
||||
const normalizedHex = candidateHex.toLowerCase();
|
||||
const label =
|
||||
candidateNpub && candidateNpub.startsWith("npub1")
|
||||
? candidateNpub
|
||||
: this.safeEncodeNpub(normalizedHex) || normalizedHex;
|
||||
pushEntry(normalizedHex, label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (candidateNpub && candidateNpub.startsWith("npub1")) {
|
||||
const decoded = this.safeDecodeNpub(candidateNpub);
|
||||
if (!decoded) {
|
||||
return;
|
||||
}
|
||||
const label = this.safeEncodeNpub(decoded) || candidateNpub;
|
||||
pushEntry(decoded, label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const deduped = [];
|
||||
const seenHex = new Set();
|
||||
normalizedEntries.forEach((entry) => {
|
||||
if (!seenHex.has(entry.hex)) {
|
||||
seenHex.add(entry.hex);
|
||||
deduped.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
this.profileBlockedList.innerHTML = "";
|
||||
|
||||
if (!entries.length) {
|
||||
if (!deduped.length) {
|
||||
this.profileBlockedEmpty.classList.remove("hidden");
|
||||
this.profileBlockedList.classList.add("hidden");
|
||||
return;
|
||||
@@ -3772,49 +3883,155 @@ class bitvidApp {
|
||||
this.profileBlockedEmpty.classList.add("hidden");
|
||||
this.profileBlockedList.classList.remove("hidden");
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const identifier =
|
||||
typeof entry === "string"
|
||||
? entry
|
||||
: typeof entry?.npub === "string" && entry.npub
|
||||
? entry.npub
|
||||
: typeof entry?.pubkey === "string"
|
||||
? entry.pubkey
|
||||
: "";
|
||||
|
||||
if (!identifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
deduped.forEach(({ hex, label }) => {
|
||||
const item = document.createElement("li");
|
||||
item.className =
|
||||
"flex items-center justify-between gap-4 rounded-lg bg-gray-800 px-4 py-3";
|
||||
|
||||
const label = document.createElement("div");
|
||||
label.className = "min-w-0";
|
||||
const info = document.createElement("div");
|
||||
info.className = "min-w-0";
|
||||
|
||||
const title = document.createElement("p");
|
||||
title.className = "text-sm font-medium text-gray-100 break-all";
|
||||
title.textContent = identifier;
|
||||
title.textContent = label;
|
||||
|
||||
label.appendChild(title);
|
||||
info.appendChild(title);
|
||||
|
||||
const actionBtn = document.createElement("button");
|
||||
actionBtn.type = "button";
|
||||
actionBtn.className =
|
||||
"px-3 py-1 rounded-md bg-gray-700 text-xs font-medium text-gray-100 hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900";
|
||||
actionBtn.textContent = "Remove";
|
||||
actionBtn.dataset.blockedHex = hex;
|
||||
actionBtn.addEventListener("click", () => {
|
||||
this.showSuccess("Block management coming soon.");
|
||||
this.handleRemoveBlockedCreator(hex);
|
||||
});
|
||||
|
||||
item.appendChild(label);
|
||||
item.appendChild(info);
|
||||
item.appendChild(actionBtn);
|
||||
|
||||
this.profileBlockedList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async handleAddBlockedCreator() {
|
||||
if (!this.profileBlockedInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawValue = this.profileBlockedInput.value;
|
||||
const trimmed = typeof rawValue === "string" ? rawValue.trim() : "";
|
||||
|
||||
if (!trimmed) {
|
||||
this.showError("Enter an npub to block.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pubkey) {
|
||||
this.showError("Please login to manage your block list.");
|
||||
return;
|
||||
}
|
||||
|
||||
const actorHex = this.pubkey;
|
||||
let targetHex = "";
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
targetHex = this.safeDecodeNpub(trimmed) || "";
|
||||
if (!targetHex) {
|
||||
this.showError("Invalid npub. Please double-check and try again.");
|
||||
return;
|
||||
}
|
||||
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
targetHex = trimmed.toLowerCase();
|
||||
} else {
|
||||
this.showError("Enter a valid npub or hex pubkey.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetHex === actorHex) {
|
||||
this.showError("You cannot block yourself.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await userBlocks.ensureLoaded(actorHex);
|
||||
|
||||
if (userBlocks.isBlocked(targetHex)) {
|
||||
this.showSuccess("You already blocked this creator.");
|
||||
} else {
|
||||
await userBlocks.addBlock(targetHex, actorHex);
|
||||
this.showSuccess(
|
||||
"Creator blocked. You won't see their videos anymore."
|
||||
);
|
||||
}
|
||||
|
||||
this.profileBlockedInput.value = "";
|
||||
this.populateBlockedList();
|
||||
await this.loadVideos();
|
||||
} catch (error) {
|
||||
console.error("Failed to add creator to personal block list:", error);
|
||||
const message =
|
||||
error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: "Failed to update your block list. Please try again.";
|
||||
this.showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRemoveBlockedCreator(candidate) {
|
||||
if (!this.pubkey) {
|
||||
this.showError("Please login to manage your block list.");
|
||||
return;
|
||||
}
|
||||
|
||||
let targetHex = "";
|
||||
if (typeof candidate === "string") {
|
||||
const trimmed = candidate.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
targetHex = this.safeDecodeNpub(trimmed) || "";
|
||||
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
targetHex = trimmed.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetHex) {
|
||||
console.warn("No valid pubkey to remove from block list:", candidate);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await userBlocks.ensureLoaded(this.pubkey);
|
||||
|
||||
if (!userBlocks.isBlocked(targetHex)) {
|
||||
this.showSuccess("Creator already removed from your block list.");
|
||||
} else {
|
||||
await userBlocks.removeBlock(targetHex, this.pubkey);
|
||||
this.showSuccess("Creator removed from your block list.");
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
await this.loadVideos();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to remove creator from personal block list:",
|
||||
error
|
||||
);
|
||||
const message =
|
||||
error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: "Failed to update your block list. Please try again.";
|
||||
this.showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
isAuthorBlocked(pubkey) {
|
||||
return userBlocks.isBlocked(pubkey);
|
||||
}
|
||||
|
||||
async refreshAdminPaneState() {
|
||||
const adminNav = this.profileNavButtons.admin;
|
||||
const adminPane = this.profilePaneElements.admin;
|
||||
@@ -5037,6 +5254,14 @@ class bitvidApp {
|
||||
|
||||
await this.refreshAdminPaneState();
|
||||
|
||||
try {
|
||||
await userBlocks.loadBlocks(pubkey);
|
||||
} catch (error) {
|
||||
console.warn("Failed to load personal block list:", error);
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
|
||||
// Hide login button if present
|
||||
if (this.loginButton) {
|
||||
this.loginButton.classList.add("hidden");
|
||||
@@ -5082,6 +5307,9 @@ class bitvidApp {
|
||||
this.pubkey = null;
|
||||
this.currentUserNpub = null;
|
||||
|
||||
userBlocks.reset();
|
||||
this.populateBlockedList();
|
||||
|
||||
// Show the login button again
|
||||
if (this.loginButton) {
|
||||
this.loginButton.classList.remove("hidden");
|
||||
@@ -5573,6 +5801,10 @@ class bitvidApp {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isAuthorBlocked(video.pubkey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!accessControl.canAccess(video)) {
|
||||
return false;
|
||||
}
|
||||
@@ -6943,7 +7175,62 @@ class bitvidApp {
|
||||
break;
|
||||
}
|
||||
case "block-author": {
|
||||
this.showSuccess("Block management coming soon.");
|
||||
if (!this.pubkey) {
|
||||
this.showError("Please login to manage your block list.");
|
||||
break;
|
||||
}
|
||||
|
||||
const authorCandidate =
|
||||
dataset.author ||
|
||||
(this.currentVideo && this.currentVideo.pubkey) ||
|
||||
"";
|
||||
|
||||
const trimmed =
|
||||
typeof authorCandidate === "string" ? authorCandidate.trim() : "";
|
||||
if (!trimmed) {
|
||||
this.showError("Unable to determine the creator to block.");
|
||||
break;
|
||||
}
|
||||
|
||||
let normalizedHex = "";
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
normalizedHex = this.safeDecodeNpub(trimmed) || "";
|
||||
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
normalizedHex = trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
if (!normalizedHex) {
|
||||
this.showError("Unable to determine the creator to block.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (normalizedHex === this.pubkey) {
|
||||
this.showError("You cannot block yourself.");
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
await userBlocks.ensureLoaded(this.pubkey);
|
||||
|
||||
if (userBlocks.isBlocked(normalizedHex)) {
|
||||
this.showSuccess("You already blocked this creator.");
|
||||
} else {
|
||||
await userBlocks.addBlock(normalizedHex, this.pubkey);
|
||||
this.showSuccess(
|
||||
"Creator blocked. You won't see their videos anymore."
|
||||
);
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
await this.loadVideos();
|
||||
} catch (error) {
|
||||
console.error("Failed to update personal block list:", error);
|
||||
const message =
|
||||
error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: "Failed to update your block list. Please try again.";
|
||||
this.showError(message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "report": {
|
||||
@@ -8221,6 +8508,28 @@ class bitvidApp {
|
||||
}
|
||||
}
|
||||
|
||||
safeDecodeNpub(npub) {
|
||||
if (typeof npub !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = npub.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = window.NostrTools.nip19.decode(trimmed);
|
||||
if (decoded.type === "npub" && typeof decoded.data === "string") {
|
||||
return decoded.data;
|
||||
}
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to fetch an older event by its ID if we can't find it in
|
||||
* this.videosMap or from a bulk fetch. Uses nostrClient.getEventById.
|
||||
|
||||
+18
-3
@@ -279,7 +279,22 @@ class SubscriptionsManager {
|
||||
window.app?.dedupeVideosByRoot?.(safeVideos) ??
|
||||
this.dedupeToNewestByRoot(safeVideos);
|
||||
|
||||
if (!dedupedVideos.length) {
|
||||
const filteredVideos = dedupedVideos.filter((video) => {
|
||||
if (!video || typeof video !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
window.app?.isAuthorBlocked &&
|
||||
window.app.isAuthorBlocked(video.pubkey)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!filteredVideos.length) {
|
||||
container.innerHTML = `
|
||||
<p class="flex justify-center items-center h-full w-full text-center text-gray-500">
|
||||
No videos available yet.
|
||||
@@ -288,14 +303,14 @@ class SubscriptionsManager {
|
||||
}
|
||||
|
||||
// Sort newest first
|
||||
dedupedVideos.sort((a, b) => b.created_at - a.created_at);
|
||||
filteredVideos.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
const fullAllEventsArray = Array.from(nostrClient.allEvents.values());
|
||||
const fragment = document.createDocumentFragment();
|
||||
// Only declare localAuthorSet once
|
||||
const localAuthorSet = new Set();
|
||||
|
||||
dedupedVideos.forEach((video, index) => {
|
||||
filteredVideos.forEach((video, index) => {
|
||||
if (!video.id || !video.title) {
|
||||
console.error("Missing ID or title:", video);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
// js/userBlocks.js
|
||||
import { nostrClient } from "./nostr.js";
|
||||
|
||||
const BLOCK_LIST_IDENTIFIER = "user-blocks";
|
||||
|
||||
function normalizeHex(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class UserBlockListManager {
|
||||
constructor() {
|
||||
this.blockedPubkeys = new Set();
|
||||
this.blockEventId = null;
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.blockedPubkeys.clear();
|
||||
this.blockEventId = null;
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
getBlockedPubkeys() {
|
||||
return Array.from(this.blockedPubkeys);
|
||||
}
|
||||
|
||||
isBlocked(pubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return this.blockedPubkeys.has(normalized);
|
||||
}
|
||||
|
||||
async ensureLoaded(userPubkey) {
|
||||
if (this.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadBlocks(normalized);
|
||||
}
|
||||
|
||||
async loadBlocks(userPubkey) {
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
if (!normalized) {
|
||||
this.reset();
|
||||
this.loaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window?.nostr?.nip04?.decrypt) {
|
||||
console.warn(
|
||||
"[UserBlockList] nip04.decrypt is unavailable; treating block list as empty."
|
||||
);
|
||||
this.reset();
|
||||
this.loaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const filter = {
|
||||
kinds: [30002],
|
||||
authors: [normalized],
|
||||
"#d": [BLOCK_LIST_IDENTIFIER],
|
||||
limit: 1,
|
||||
};
|
||||
|
||||
const events = [];
|
||||
for (const relay of nostrClient.relays) {
|
||||
try {
|
||||
const res = await nostrClient.pool.list([relay], [filter]);
|
||||
if (Array.isArray(res) && res.length) {
|
||||
events.push(...res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[UserBlockList] Relay error at ${relay}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!events.length) {
|
||||
this.blockedPubkeys.clear();
|
||||
this.blockEventId = null;
|
||||
this.loaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
events.sort((a, b) => b.created_at - a.created_at);
|
||||
const newest = events[0];
|
||||
this.blockEventId = newest.id;
|
||||
|
||||
let decrypted = "";
|
||||
try {
|
||||
decrypted = await window.nostr.nip04.decrypt(
|
||||
normalized,
|
||||
newest.content
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[UserBlockList] Failed to decrypt block list:", err);
|
||||
this.blockedPubkeys.clear();
|
||||
this.loaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(decrypted);
|
||||
const list = Array.isArray(parsed?.blockedPubkeys)
|
||||
? parsed.blockedPubkeys
|
||||
: [];
|
||||
const sanitized = list
|
||||
.map((entry) => normalizeHex(entry))
|
||||
.filter((candidate) => {
|
||||
if (!candidate) {
|
||||
return false;
|
||||
}
|
||||
if (candidate === normalized) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.blockedPubkeys = new Set(sanitized);
|
||||
} catch (err) {
|
||||
console.error("[UserBlockList] Failed to parse block list:", err);
|
||||
this.blockedPubkeys.clear();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[UserBlockList] loadBlocks failed:", error);
|
||||
this.blockedPubkeys.clear();
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
async addBlock(targetPubkey, userPubkey) {
|
||||
const actorHex = normalizeHex(userPubkey);
|
||||
if (!actorHex) {
|
||||
throw new Error("Invalid user pubkey.");
|
||||
}
|
||||
|
||||
const targetHex = normalizeHex(targetPubkey);
|
||||
if (!targetHex) {
|
||||
const err = new Error("Invalid target pubkey.");
|
||||
err.code = "invalid";
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (actorHex === targetHex) {
|
||||
const err = new Error("Cannot block yourself.");
|
||||
err.code = "self";
|
||||
throw err;
|
||||
}
|
||||
|
||||
await this.ensureLoaded(actorHex);
|
||||
|
||||
if (this.blockedPubkeys.has(targetHex)) {
|
||||
return { ok: true, already: true };
|
||||
}
|
||||
|
||||
const snapshot = new Set(this.blockedPubkeys);
|
||||
this.blockedPubkeys.add(targetHex);
|
||||
|
||||
try {
|
||||
await this.publishBlockList(actorHex);
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
this.blockedPubkeys = snapshot;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async removeBlock(targetPubkey, userPubkey) {
|
||||
const actorHex = normalizeHex(userPubkey);
|
||||
if (!actorHex) {
|
||||
throw new Error("Invalid user pubkey.");
|
||||
}
|
||||
|
||||
const targetHex = normalizeHex(targetPubkey);
|
||||
if (!targetHex) {
|
||||
return { ok: true, already: true };
|
||||
}
|
||||
|
||||
await this.ensureLoaded(actorHex);
|
||||
|
||||
if (!this.blockedPubkeys.has(targetHex)) {
|
||||
return { ok: true, already: true };
|
||||
}
|
||||
|
||||
const snapshot = new Set(this.blockedPubkeys);
|
||||
this.blockedPubkeys.delete(targetHex);
|
||||
|
||||
try {
|
||||
await this.publishBlockList(actorHex);
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
this.blockedPubkeys = snapshot;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async publishBlockList(userPubkey) {
|
||||
if (!window?.nostr?.nip04?.encrypt) {
|
||||
const err = new Error(
|
||||
"NIP-04 encryption is required to update the block list."
|
||||
);
|
||||
err.code = "nip04-missing";
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (typeof window.nostr.signEvent !== "function") {
|
||||
const err = new Error("Nostr extension missing signEvent support.");
|
||||
err.code = "nip04-missing";
|
||||
throw err;
|
||||
}
|
||||
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
if (!normalized) {
|
||||
throw new Error("Invalid user pubkey.");
|
||||
}
|
||||
|
||||
const payload = {
|
||||
blockedPubkeys: Array.from(this.blockedPubkeys).filter(
|
||||
(candidate) => candidate && candidate !== normalized
|
||||
),
|
||||
};
|
||||
const plaintext = JSON.stringify(payload);
|
||||
|
||||
let cipherText = "";
|
||||
try {
|
||||
cipherText = await window.nostr.nip04.encrypt(normalized, plaintext);
|
||||
} catch (error) {
|
||||
const err = new Error("Failed to encrypt block list.");
|
||||
err.code = "nip04-missing";
|
||||
throw err;
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: 30002,
|
||||
pubkey: normalized,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [["d", BLOCK_LIST_IDENTIFIER]],
|
||||
content: cipherText,
|
||||
};
|
||||
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
|
||||
await Promise.all(
|
||||
nostrClient.relays.map(async (relay) => {
|
||||
try {
|
||||
await nostrClient.pool.publish([relay], signedEvent);
|
||||
} catch (err) {
|
||||
console.error(`[UserBlockList] Failed to publish to ${relay}:`, err);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.blockEventId = signedEvent.id;
|
||||
return signedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export const userBlocks = new UserBlockListManager();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.userBlocks = userBlocks;
|
||||
}
|
||||
Reference in new issue
Block a user