mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-07 21:32:51 +00:00
chore(ai): add automated PR review report
Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
c3f96c8819
commit
cfbbf78941
30 files changed
+616
-1765
No files matched your search
@@ -3272,25 +3272,17 @@ class Application {
|
||||
);
|
||||
}
|
||||
|
||||
const activePubkey =
|
||||
this.normalizeHexPubkey(getActiveProfilePubkey()) || this.pubkey;
|
||||
if (
|
||||
activePubkey &&
|
||||
this.nostrService &&
|
||||
typeof this.nostrService.loadDirectMessages === "function"
|
||||
) {
|
||||
this.nostrService
|
||||
.loadDirectMessages({
|
||||
actorPubkey: activePubkey,
|
||||
limit: 50,
|
||||
initialLoad: true,
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to sync direct messages during login:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
try {
|
||||
await this.nostrService.loadDirectMessages({
|
||||
actorPubkey: activePubkey,
|
||||
limit: 50,
|
||||
initialLoad: true,
|
||||
});
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to sync direct messages during login:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4271,6 +4263,13 @@ class Application {
|
||||
devLogger.error("Post-login hydration failed:", error);
|
||||
});
|
||||
|
||||
let postLoginResult = null;
|
||||
try {
|
||||
postLoginResult = await postLoginPromise;
|
||||
} catch (error) {
|
||||
devLogger.error("Post-login processing failed:", error);
|
||||
}
|
||||
|
||||
await nwcPromise;
|
||||
// We do not await hashtagPreferencesPromise here; it loads in the background.
|
||||
|
||||
@@ -4283,26 +4282,6 @@ class Application {
|
||||
);
|
||||
}
|
||||
|
||||
const scheduleLoginRefresh = (reason) => {
|
||||
try {
|
||||
this.lastIdentityRefreshPromise = this.refreshAllVideoGrids({
|
||||
reason,
|
||||
forceMainReload: true,
|
||||
});
|
||||
return this.lastIdentityRefreshPromise;
|
||||
} catch (error) {
|
||||
devLogger.error("Failed to refresh video grids after login:", error);
|
||||
this.lastIdentityRefreshPromise = null;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
this.reinitializeVideoListView({ reason: "login" });
|
||||
} catch (error) {
|
||||
devLogger.warn("Failed to reinitialize video list view after login:", error);
|
||||
}
|
||||
|
||||
if (activePubkey) {
|
||||
const aggregatedBlacklist = accessControl.getBlacklist();
|
||||
|
||||
@@ -4335,40 +4314,16 @@ class Application {
|
||||
// If it times out, the UI will refresh reactively when the data arrives.
|
||||
const blockLoadPromise = userBlocks.ensureLoaded(activePubkey);
|
||||
const blockLoadTimeout = new Promise((resolve) =>
|
||||
setTimeout(() => resolve({ timedOut: true }), 2000)
|
||||
setTimeout(resolve, 2000)
|
||||
);
|
||||
|
||||
Promise.race([
|
||||
blockLoadPromise
|
||||
.then(() => ({ timedOut: false }))
|
||||
.catch((error) => ({ error })),
|
||||
blockLoadTimeout,
|
||||
])
|
||||
.then((result) => {
|
||||
if (result?.error) {
|
||||
devLogger.warn(
|
||||
"[Application] Block list load timed out or failed:",
|
||||
result.error
|
||||
);
|
||||
} else if (result?.timedOut) {
|
||||
devLogger.warn(
|
||||
"[Application] Block list load timed out:",
|
||||
result
|
||||
);
|
||||
}
|
||||
const refreshPromise = scheduleLoginRefresh("auth-login");
|
||||
if (refreshPromise && typeof refreshPromise.finally === "function") {
|
||||
refreshPromise.finally(() => {
|
||||
this.lastIdentityRefreshPromise = null;
|
||||
});
|
||||
} else {
|
||||
this.lastIdentityRefreshPromise = null;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.error("Failed to refresh video grids after login:", error);
|
||||
this.lastIdentityRefreshPromise = null;
|
||||
});
|
||||
try {
|
||||
await Promise.race([blockLoadPromise, blockLoadTimeout]);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[Application] Block list load timed out or failed:",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Load subscriptions in the background to avoid blocking the main feed.
|
||||
subscriptions.ensureLoaded(activePubkey).catch((error) => {
|
||||
@@ -4377,33 +4332,24 @@ class Application {
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.nostrService &&
|
||||
typeof this.nostrService.loadDirectMessages === "function"
|
||||
) {
|
||||
this.nostrService
|
||||
.loadDirectMessages({
|
||||
actorPubkey: activePubkey,
|
||||
limit: 50,
|
||||
initialLoad: true,
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to sync direct messages during login:",
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const refreshPromise = scheduleLoginRefresh("auth-login");
|
||||
if (refreshPromise && typeof refreshPromise.finally === "function") {
|
||||
refreshPromise.finally(() => {
|
||||
this.lastIdentityRefreshPromise = null;
|
||||
});
|
||||
} else {
|
||||
this.lastIdentityRefreshPromise = null;
|
||||
}
|
||||
try {
|
||||
this.reinitializeVideoListView({ reason: "login", postLoginResult });
|
||||
} catch (error) {
|
||||
devLogger.warn("Failed to reinitialize video list view after login:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
this.lastIdentityRefreshPromise = this.refreshAllVideoGrids({
|
||||
reason: "auth-login",
|
||||
forceMainReload: true,
|
||||
});
|
||||
await this.lastIdentityRefreshPromise;
|
||||
} catch (error) {
|
||||
devLogger.error("Failed to refresh video grids after login:", error);
|
||||
} finally {
|
||||
this.lastIdentityRefreshPromise = null;
|
||||
}
|
||||
|
||||
this.forceRefreshAllProfiles();
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS,
|
||||
DEFAULT_PLAYBACK_SOURCE,
|
||||
DEFAULT_PLAYBACK_START_TIMEOUT,
|
||||
CLEAR_NIP07_PERMISSIONS_ON_LOGOUT,
|
||||
} from "../config/instance-config.js";
|
||||
|
||||
export const WATCH_HISTORY_VERSION_TAG_VALUE = "2";
|
||||
@@ -138,4 +137,3 @@ export { VIEW_COUNT_CACHE_TTL_MS };
|
||||
export { ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS };
|
||||
export { DEFAULT_PLAYBACK_SOURCE };
|
||||
export { DEFAULT_PLAYBACK_START_TIMEOUT };
|
||||
export { CLEAR_NIP07_PERMISSIONS_ON_LOGOUT };
|
||||
+56
-209
@@ -17,7 +17,7 @@
|
||||
* - Signer Management: Orchestrates NIP-07 (Extension), NIP-46 (Remote/Bunker), and NIP-01 (Local nsec) signers.
|
||||
*/
|
||||
|
||||
import { CLEAR_NIP07_PERMISSIONS_ON_LOGOUT, isDevMode } from "../config.js";
|
||||
import { isDevMode } from "../config.js";
|
||||
import { FEATURE_PUBLISH_NIP71 } from "../constants.js";
|
||||
import { bytesToHex, sha256 } from "../../vendor/crypto-helpers.bundle.min.js";
|
||||
import { infoHashFromMagnet } from "../magnets.js";
|
||||
@@ -201,7 +201,6 @@ import {
|
||||
clearActiveSigner as clearActiveSignerInRegistry,
|
||||
logoutSigner as logoutSignerFromRegistry,
|
||||
resolveActiveSigner as resolveActiveSignerFromRegistry,
|
||||
requestDefaultExtensionPermissions,
|
||||
} from "../nostrClientRegistry.js";
|
||||
import { createNip07Adapter } from "./adapters/nip07Adapter.js";
|
||||
import { createNsecAdapter } from "./adapters/nsecAdapter.js";
|
||||
@@ -396,16 +395,6 @@ function shouldRequestExtensionPermissions(signer) {
|
||||
return signer.type === "extension";
|
||||
}
|
||||
|
||||
function normalizePermissionMethods(methods) {
|
||||
if (!Array.isArray(methods)) {
|
||||
return [];
|
||||
}
|
||||
const normalized = methods
|
||||
.map((method) => normalizePermissionMethod(method))
|
||||
.filter(Boolean);
|
||||
return Array.from(new Set(normalized));
|
||||
}
|
||||
|
||||
const EVENTS_CACHE_STORAGE_KEY = "bitvid:eventsCache:v1";
|
||||
// We use the policy TTL, but currently the storage backend is hardcoded to IDB (with localStorage fallback).
|
||||
// Future refactors should make EventsCacheStore dynamic based on CACHE_POLICIES[NOTE_TYPES.VIDEO_POST].storage.
|
||||
@@ -1017,63 +1006,6 @@ function buildDmFilters(actorPubkey, { since, until, limit } = {}) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
function normalizeDmEncryptionHint(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
return value.trim().toLowerCase().replace(/[^a-z0-9_.-]/g, "");
|
||||
}
|
||||
|
||||
function resolveDmDecryptPermissionMethods(event) {
|
||||
if (!event || typeof event !== "object") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const kind = Number.isFinite(event.kind) ? event.kind : null;
|
||||
if (kind === 1059) {
|
||||
return ["nip44.decrypt", "nip44.v2.decrypt"];
|
||||
}
|
||||
|
||||
const tags = Array.isArray(event.tags) ? event.tags : [];
|
||||
let requiresNip44 = false;
|
||||
let requiresNip04 = false;
|
||||
|
||||
for (const tag of tags) {
|
||||
if (!Array.isArray(tag) || tag.length < 2) {
|
||||
continue;
|
||||
}
|
||||
const label = typeof tag[0] === "string" ? tag[0].trim().toLowerCase() : "";
|
||||
if (label !== "encrypted" && label !== "encryption") {
|
||||
continue;
|
||||
}
|
||||
for (let index = 1; index < tag.length; index += 1) {
|
||||
const normalized = normalizeDmEncryptionHint(tag[index]);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
if (normalized.includes("nip44")) {
|
||||
requiresNip44 = true;
|
||||
}
|
||||
if (normalized.includes("nip04")) {
|
||||
requiresNip04 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!requiresNip04 && !requiresNip44 && kind === 4) {
|
||||
requiresNip04 = true;
|
||||
}
|
||||
|
||||
const methods = [];
|
||||
if (requiresNip04) {
|
||||
methods.push("nip04.decrypt");
|
||||
}
|
||||
if (requiresNip44) {
|
||||
methods.push("nip44.decrypt", "nip44.v2.decrypt");
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
|
||||
const EXTENSION_MIME_MAP = Object.freeze(
|
||||
Object.fromEntries(
|
||||
@@ -1224,9 +1156,7 @@ export class NostrClient {
|
||||
getActivePubkey: () => this.pubkey,
|
||||
getSessionActor: () => this.sessionActor,
|
||||
ensureSessionActor: () => this.ensureSessionActor(),
|
||||
ensureExtensionPermissions: (...args) => this.ensureExtensionPermissionsGate(...args),
|
||||
hasRequiredExtensionPermissions: (...args) =>
|
||||
this.hasRequiredExtensionPermissions(...args),
|
||||
ensureExtensionPermissions: (...args) => this.ensureExtensionPermissions(...args),
|
||||
resolveActiveSigner,
|
||||
shouldRequestExtensionPermissions,
|
||||
signEventWithPrivateKey,
|
||||
@@ -1336,7 +1266,6 @@ export class NostrClient {
|
||||
this.pendingHandshakeCancel = null;
|
||||
this.remoteSignerListeners = new Set();
|
||||
this.sessionActorListeners = new Set();
|
||||
this.extensionPermissionRequestPromise = null;
|
||||
const storedRemoteSigner = this.getStoredNip46Metadata();
|
||||
this.remoteSignerStatus = {
|
||||
state: storedRemoteSigner.hasSession ? "stored" : "idle",
|
||||
@@ -2025,12 +1954,18 @@ export class NostrClient {
|
||||
);
|
||||
}
|
||||
|
||||
const initialPubkey = await runNip07WithRetry(
|
||||
() => extension.getPublicKey(),
|
||||
{
|
||||
label: "extension.getPublicKey",
|
||||
},
|
||||
const permissionResult = await this.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
);
|
||||
if (!permissionResult.ok) {
|
||||
const denialMessage =
|
||||
'The NIP-07 extension reported "permission denied". Please approve the prompt and try again.';
|
||||
const denialError = new Error(denialMessage);
|
||||
if (permissionResult.error) {
|
||||
denialError.cause = permissionResult.error;
|
||||
}
|
||||
throw denialError;
|
||||
}
|
||||
|
||||
if (
|
||||
allowAccountSelection &&
|
||||
@@ -2061,12 +1996,9 @@ export class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
const pubkey =
|
||||
allowAccountSelection && typeof extension.selectAccounts === "function"
|
||||
? await runNip07WithRetry(() => extension.getPublicKey(), {
|
||||
label: "extension.getPublicKey",
|
||||
})
|
||||
: initialPubkey;
|
||||
const pubkey = await runNip07WithRetry(() => extension.getPublicKey(), {
|
||||
label: "extension.getPublicKey",
|
||||
});
|
||||
|
||||
if (!pubkey || typeof pubkey !== "string") {
|
||||
throw new Error(
|
||||
@@ -2103,19 +2035,19 @@ export class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
const permissionResult = await this.ensureExtensionPermissions([
|
||||
...DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
...DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
]);
|
||||
|
||||
if (!permissionResult.ok) {
|
||||
throw permissionResult.error;
|
||||
}
|
||||
|
||||
const adapter = await createNip07Adapter(extension);
|
||||
adapter.pubkey = pubkey;
|
||||
setActiveSigner(adapter);
|
||||
|
||||
this.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS).catch(
|
||||
(err) => {
|
||||
userLogger.warn(
|
||||
"[nostr] Extension permissions were not fully granted after login:",
|
||||
err,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return { pubkey, signer: adapter };
|
||||
}
|
||||
|
||||
@@ -2856,8 +2788,16 @@ export class NostrClient {
|
||||
this.extensionPermissionCache = new Set();
|
||||
}
|
||||
|
||||
if (!Array.isArray(methods)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let didChange = false;
|
||||
for (const normalized of normalizePermissionMethods(methods)) {
|
||||
for (const method of methods) {
|
||||
const normalized = normalizePermissionMethod(method);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
if (!this.extensionPermissionCache.has(normalized)) {
|
||||
didChange = true;
|
||||
}
|
||||
@@ -2873,50 +2813,20 @@ export class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
hasRequiredExtensionPermissions(
|
||||
methods = DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
) {
|
||||
const normalized = normalizePermissionMethods(methods);
|
||||
if (!normalized.length) {
|
||||
return true;
|
||||
}
|
||||
if (!this.extensionPermissionCache) {
|
||||
return false;
|
||||
}
|
||||
return normalized.every((method) => this.extensionPermissionCache.has(method));
|
||||
}
|
||||
|
||||
async ensureExtensionPermissionsGate(
|
||||
methods = DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
) {
|
||||
const normalized = normalizePermissionMethods(methods);
|
||||
if (!normalized.length) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (this.hasRequiredExtensionPermissions(normalized)) {
|
||||
return { ok: true, cached: true };
|
||||
}
|
||||
|
||||
if (this.extensionPermissionRequestPromise) {
|
||||
return this.extensionPermissionRequestPromise;
|
||||
}
|
||||
|
||||
const requestPromise = this.ensureExtensionPermissions(normalized)
|
||||
.catch((error) => ({ ok: false, error }))
|
||||
.finally(() => {
|
||||
if (this.extensionPermissionRequestPromise === requestPromise) {
|
||||
this.extensionPermissionRequestPromise = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.extensionPermissionRequestPromise = requestPromise;
|
||||
|
||||
return requestPromise;
|
||||
}
|
||||
|
||||
async ensureExtensionPermissions(methods = DEFAULT_NIP07_PERMISSION_METHODS) {
|
||||
const normalized = normalizePermissionMethods(methods);
|
||||
if (!Array.isArray(methods)) {
|
||||
methods = [];
|
||||
}
|
||||
|
||||
const normalized = Array.from(
|
||||
new Set(
|
||||
methods
|
||||
.map((method) =>
|
||||
typeof method === "string" && method.trim() ? method.trim() : "",
|
||||
)
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
if (!normalized.length) {
|
||||
return { ok: true };
|
||||
@@ -4463,9 +4373,9 @@ export class NostrClient {
|
||||
* - Wipes session actor (ephemeral keys).
|
||||
* - Disconnects NIP-46 remote signer (if active).
|
||||
* - Clears Watch History cache.
|
||||
* - Optionally clears stored NIP-07 permissions.
|
||||
* - Resets permissions cache.
|
||||
*/
|
||||
logout({ clearNip07Permissions = null } = {}) {
|
||||
logout() {
|
||||
const previousPubkey = this.pubkey;
|
||||
this.pubkey = null;
|
||||
logoutSigner(previousPubkey);
|
||||
@@ -4499,20 +4409,13 @@ export class NostrClient {
|
||||
}
|
||||
|
||||
this.watchHistory.clear();
|
||||
const shouldClearNip07Permissions =
|
||||
typeof clearNip07Permissions === "boolean"
|
||||
? clearNip07Permissions
|
||||
: CLEAR_NIP07_PERMISSIONS_ON_LOGOUT;
|
||||
|
||||
if (shouldClearNip07Permissions) {
|
||||
if (
|
||||
this.extensionPermissionCache &&
|
||||
typeof this.extensionPermissionCache.clear === "function"
|
||||
) {
|
||||
this.extensionPermissionCache.clear();
|
||||
}
|
||||
clearStoredNip07Permissions();
|
||||
if (
|
||||
this.extensionPermissionCache &&
|
||||
typeof this.extensionPermissionCache.clear === "function"
|
||||
) {
|
||||
this.extensionPermissionCache.clear();
|
||||
}
|
||||
clearStoredNip07Permissions();
|
||||
devLogger.log("User logged out.");
|
||||
}
|
||||
|
||||
@@ -4717,10 +4620,7 @@ export class NostrClient {
|
||||
return { actorPubkey: normalizedActor, decryptors };
|
||||
}
|
||||
|
||||
async decryptDirectMessageEvent(
|
||||
event,
|
||||
{ actorPubkey, onPermissionRequest } = {},
|
||||
) {
|
||||
async decryptDirectMessageEvent(event, { actorPubkey } = {}) {
|
||||
const eventId = typeof event?.id === "string" ? event.id : "";
|
||||
if (eventId) {
|
||||
const cached = this.dmDecryptCache.get(eventId);
|
||||
@@ -4731,50 +4631,6 @@ export class NostrClient {
|
||||
|
||||
const decryptDM = await this.ensureDmDecryptor();
|
||||
const context = await this.buildDmDecryptContext(actorPubkey);
|
||||
const permissionMethods = resolveDmDecryptPermissionMethods(event);
|
||||
const needsExtensionPermission =
|
||||
permissionMethods.length &&
|
||||
typeof window !== "undefined" &&
|
||||
window?.nostr &&
|
||||
!this.hasRequiredExtensionPermissions(permissionMethods) &&
|
||||
context.decryptors.some(
|
||||
(candidate) => candidate?.source === "extension",
|
||||
);
|
||||
|
||||
if (needsExtensionPermission) {
|
||||
if (typeof onPermissionRequest === "function") {
|
||||
onPermissionRequest({
|
||||
status: "decrypting",
|
||||
methods: permissionMethods,
|
||||
eventId,
|
||||
});
|
||||
}
|
||||
|
||||
const permissionResult =
|
||||
await requestDefaultExtensionPermissions(permissionMethods);
|
||||
if (!permissionResult?.ok) {
|
||||
const error = new Error("extension-permission-denied");
|
||||
error.code = "extension-permission-denied";
|
||||
error.cause = permissionResult?.error || null;
|
||||
if (typeof onPermissionRequest === "function") {
|
||||
onPermissionRequest({
|
||||
status: "error",
|
||||
methods: permissionMethods,
|
||||
eventId,
|
||||
error,
|
||||
});
|
||||
}
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
if (typeof onPermissionRequest === "function") {
|
||||
onPermissionRequest({
|
||||
status: "ready",
|
||||
methods: permissionMethods,
|
||||
eventId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
@@ -4808,10 +4664,6 @@ export class NostrClient {
|
||||
Number.isFinite(options?.decryptLimit) && options.decryptLimit > 0
|
||||
? Math.floor(options.decryptLimit)
|
||||
: null;
|
||||
const onPermissionRequest =
|
||||
typeof options?.onPermissionRequest === "function"
|
||||
? options.onPermissionRequest
|
||||
: null;
|
||||
|
||||
const relayCandidates = Array.isArray(options.relays)
|
||||
? options.relays
|
||||
@@ -4864,7 +4716,6 @@ export class NostrClient {
|
||||
try {
|
||||
const decrypted = await this.decryptDirectMessageEvent(event, {
|
||||
actorPubkey: context.actorPubkey || actorPubkeyInput,
|
||||
onPermissionRequest,
|
||||
});
|
||||
if (decrypted?.ok) {
|
||||
messages.push(decrypted);
|
||||
@@ -4949,10 +4800,6 @@ export class NostrClient {
|
||||
|
||||
const result = await this.decryptDirectMessageEvent(event, {
|
||||
actorPubkey: context.actorPubkey || actorPubkeyInput,
|
||||
onPermissionRequest:
|
||||
typeof options?.onPermissionRequest === "function"
|
||||
? options.onPermissionRequest
|
||||
: null,
|
||||
});
|
||||
|
||||
if (result?.ok) {
|
||||
|
||||
@@ -1213,21 +1213,10 @@ export async function publishComment(
|
||||
? shouldRequestExtensionPermissions(signer)
|
||||
: false;
|
||||
|
||||
const hasCachedPermissions =
|
||||
typeof client?.hasRequiredExtensionPermissions === "function" &&
|
||||
client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
|
||||
if (shouldRequestPermissions && !hasCachedPermissions) {
|
||||
const permissionGate =
|
||||
typeof client?.ensureExtensionPermissionsGate === "function"
|
||||
? client.ensureExtensionPermissionsGate.bind(client)
|
||||
: typeof client?.ensureExtensionPermissions === "function"
|
||||
? client.ensureExtensionPermissions.bind(client)
|
||||
: null;
|
||||
|
||||
if (permissionGate) {
|
||||
permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
if (shouldRequestPermissions) {
|
||||
permissionResult = await client.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
);
|
||||
}
|
||||
|
||||
if (!permissionResult.ok) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const nostrClient = new NostrClient();
|
||||
registerNostrClient(nostrClient, {
|
||||
requestPermissions: (
|
||||
methods = DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
) => nostrClient.ensureExtensionPermissionsGate(methods),
|
||||
) => nostrClient.ensureExtensionPermissions(methods),
|
||||
});
|
||||
|
||||
export function requestDefaultExtensionPermissions(
|
||||
@@ -24,12 +24,6 @@ export function requestDefaultExtensionPermissions(
|
||||
return requestRegisteredPermissions(methods);
|
||||
}
|
||||
|
||||
export function hasDefaultExtensionPermissions(
|
||||
methods = DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
) {
|
||||
return nostrClient.hasRequiredExtensionPermissions(methods);
|
||||
}
|
||||
|
||||
export {
|
||||
NostrClient,
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
|
||||
@@ -101,21 +101,10 @@ export async function publishDmReadReceipt(
|
||||
? shouldRequestExtensionPermissions(signer)
|
||||
: false;
|
||||
|
||||
const hasCachedPermissions =
|
||||
typeof client?.hasRequiredExtensionPermissions === "function" &&
|
||||
client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
|
||||
if (shouldRequestPermissions && !hasCachedPermissions) {
|
||||
const permissionGate =
|
||||
typeof client?.ensureExtensionPermissionsGate === "function"
|
||||
? client.ensureExtensionPermissionsGate.bind(client)
|
||||
: typeof client?.ensureExtensionPermissions === "function"
|
||||
? client.ensureExtensionPermissions.bind(client)
|
||||
: null;
|
||||
|
||||
if (permissionGate) {
|
||||
permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
if (shouldRequestPermissions) {
|
||||
permissionResult = await client.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
);
|
||||
}
|
||||
|
||||
if (!permissionResult.ok) {
|
||||
@@ -253,21 +242,10 @@ export async function publishDmTypingIndicator(
|
||||
? shouldRequestExtensionPermissions(signer)
|
||||
: false;
|
||||
|
||||
const hasCachedPermissions =
|
||||
typeof client?.hasRequiredExtensionPermissions === "function" &&
|
||||
client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
|
||||
if (shouldRequestPermissions && !hasCachedPermissions) {
|
||||
const permissionGate =
|
||||
typeof client?.ensureExtensionPermissionsGate === "function"
|
||||
? client.ensureExtensionPermissionsGate.bind(client)
|
||||
: typeof client?.ensureExtensionPermissions === "function"
|
||||
? client.ensureExtensionPermissions.bind(client)
|
||||
: null;
|
||||
|
||||
if (permissionGate) {
|
||||
permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
if (shouldRequestPermissions) {
|
||||
permissionResult = await client.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
);
|
||||
}
|
||||
|
||||
if (!permissionResult.ok) {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
|
||||
export const NIP07_LOGIN_TIMEOUT_MS = 20_000; // 20 seconds
|
||||
export const NIP07_LOGIN_TIMEOUT_MS = 60_000; // 60 seconds
|
||||
export const NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE =
|
||||
"Timed out waiting for the NIP-07 extension. Confirm the extension prompt in your browser toolbar and try again.";
|
||||
const NIP07_PERMISSIONS_STORAGE_KEY = "bitvid:nip07:permissions";
|
||||
|
||||
// Give the NIP-07 extension enough time to surface its approval prompt and let
|
||||
// users unlock/authorize it. The default is tuned to keep login responsive while
|
||||
// users unlock/authorize it. Seven seconds proved too aggressive once vendors
|
||||
// started requiring an unlock step, so we extend the window substantially while
|
||||
// still allowing manual overrides via __BITVID_NIP07_ENABLE_VARIANT_TIMEOUT_MS__.
|
||||
const DEFAULT_ENABLE_VARIANT_TIMEOUT_MS = 5_000;
|
||||
const DEFAULT_ENABLE_VARIANT_TIMEOUT_MS = 45_000;
|
||||
|
||||
export const DEFAULT_NIP07_ENCRYPTION_METHODS = Object.freeze([
|
||||
// Encryption helpers — request both legacy NIP-04 and modern NIP-44 upfront
|
||||
@@ -21,11 +22,12 @@ export const DEFAULT_NIP07_ENCRYPTION_METHODS = Object.freeze([
|
||||
]);
|
||||
|
||||
export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
|
||||
// Core auth + relay metadata (encryption permissions requested on demand)
|
||||
// Core auth + relay metadata
|
||||
"get_public_key",
|
||||
"sign_event",
|
||||
"read_relays",
|
||||
"write_relays",
|
||||
...DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
]);
|
||||
|
||||
export const NIP07_PRIORITY = Object.freeze({
|
||||
@@ -88,21 +90,6 @@ class Nip07RequestQueue {
|
||||
// while allowing critical tasks (e.g. blocklist decryption) to jump the line.
|
||||
const requestQueue = new Nip07RequestQueue();
|
||||
|
||||
export function getNip07LoginTimeoutMs() {
|
||||
const overrideValue =
|
||||
typeof globalThis !== "undefined" &&
|
||||
globalThis !== null &&
|
||||
Number.isFinite(globalThis.__BITVID_NIP07_LOGIN_TIMEOUT_MS__)
|
||||
? Math.floor(globalThis.__BITVID_NIP07_LOGIN_TIMEOUT_MS__)
|
||||
: null;
|
||||
|
||||
if (overrideValue !== null && overrideValue > 0) {
|
||||
return Math.max(1_000, overrideValue);
|
||||
}
|
||||
|
||||
return NIP07_LOGIN_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
export function getEnableVariantTimeoutMs() {
|
||||
const overrideValue =
|
||||
typeof globalThis !== "undefined" &&
|
||||
@@ -221,7 +208,7 @@ export function clearStoredNip07Permissions() {
|
||||
export function withNip07Timeout(
|
||||
operation,
|
||||
{
|
||||
timeoutMs = getNip07LoginTimeoutMs(),
|
||||
timeoutMs = NIP07_LOGIN_TIMEOUT_MS,
|
||||
message = NIP07_LOGIN_TIMEOUT_ERROR_MESSAGE,
|
||||
} = {},
|
||||
) {
|
||||
@@ -229,7 +216,7 @@ export function withNip07Timeout(
|
||||
const effectiveTimeout =
|
||||
Number.isFinite(numericTimeout) && numericTimeout > 0
|
||||
? numericTimeout
|
||||
: getNip07LoginTimeoutMs();
|
||||
: NIP07_LOGIN_TIMEOUT_MS;
|
||||
|
||||
let timeoutId;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
@@ -261,7 +248,7 @@ export async function runNip07WithRetry(
|
||||
operation,
|
||||
{
|
||||
label = "NIP-07 operation",
|
||||
timeoutMs = getNip07LoginTimeoutMs(),
|
||||
timeoutMs = NIP07_LOGIN_TIMEOUT_MS,
|
||||
retryMultiplier = 2,
|
||||
priority = NIP07_PRIORITY.NORMAL,
|
||||
} = {},
|
||||
@@ -336,29 +323,23 @@ export async function requestEnablePermissions(
|
||||
}
|
||||
permissionVariants.push(null);
|
||||
|
||||
const enableTimeoutMs = getEnableVariantTimeoutMs();
|
||||
const loginTimeoutMs = getNip07LoginTimeoutMs();
|
||||
const shortEnableTimeoutMs = Math.min(5_000, enableTimeoutMs);
|
||||
const explicitEnableTimeoutMs =
|
||||
enableTimeoutMs < 1000
|
||||
? enableTimeoutMs
|
||||
: Math.max(enableTimeoutMs, Math.min(15_000, loginTimeoutMs));
|
||||
|
||||
let lastError = null;
|
||||
for (const options of permissionVariants) {
|
||||
// Prefer the null enable() call with a shorter interactive window.
|
||||
// If it is rejected, try explicit-permission variants with a longer timeout
|
||||
// to accommodate extensions that require structured permissions.
|
||||
const variantTimeoutOverrides =
|
||||
options === null
|
||||
? {
|
||||
timeoutMs: shortEnableTimeoutMs,
|
||||
retryMultiplier: 1,
|
||||
}
|
||||
: {
|
||||
timeoutMs: explicitEnableTimeoutMs,
|
||||
retryMultiplier: 1,
|
||||
};
|
||||
// If specific permissions are requested (variants 1 & 2), fail fast (3s)
|
||||
// to avoid hanging if the extension doesn't support the structured format.
|
||||
// If we fall back to standard enable() (null), allow the full user interaction time.
|
||||
const variantTimeoutOverrides = options
|
||||
? {
|
||||
timeoutMs: Math.min(3000, getEnableVariantTimeoutMs()),
|
||||
retryMultiplier: 1,
|
||||
}
|
||||
: {
|
||||
timeoutMs: Math.min(
|
||||
NIP07_LOGIN_TIMEOUT_MS,
|
||||
getEnableVariantTimeoutMs(),
|
||||
),
|
||||
retryMultiplier: 1,
|
||||
};
|
||||
|
||||
try {
|
||||
await runNip07WithRetry(
|
||||
@@ -388,7 +369,6 @@ export const __testExports = {
|
||||
runNip07WithRetry,
|
||||
withNip07Timeout,
|
||||
getEnableVariantTimeoutMs,
|
||||
getNip07LoginTimeoutMs,
|
||||
readStoredNip07Permissions,
|
||||
writeStoredNip07Permissions,
|
||||
clearStoredNip07Permissions,
|
||||
|
||||
@@ -435,21 +435,10 @@ export async function publishVideoReaction(
|
||||
|
||||
if (canUseActiveSigner) {
|
||||
let permissionResult = { ok: true };
|
||||
const hasCachedPermissions =
|
||||
typeof client?.hasRequiredExtensionPermissions === "function" &&
|
||||
client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
|
||||
if (shouldRequestExtensionPermissions(signer) && !hasCachedPermissions) {
|
||||
const permissionGate =
|
||||
typeof client?.ensureExtensionPermissionsGate === "function"
|
||||
? client.ensureExtensionPermissionsGate.bind(client)
|
||||
: typeof client?.ensureExtensionPermissions === "function"
|
||||
? client.ensureExtensionPermissions.bind(client)
|
||||
: null;
|
||||
|
||||
if (permissionGate) {
|
||||
permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
if (shouldRequestExtensionPermissions(signer)) {
|
||||
permissionResult = await client.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS
|
||||
);
|
||||
}
|
||||
if (permissionResult.ok) {
|
||||
try {
|
||||
|
||||
+4
-15
@@ -787,21 +787,10 @@ export async function publishViewEvent(
|
||||
|
||||
if (canUseActiveSigner) {
|
||||
let permissionResult = { ok: true };
|
||||
const hasCachedPermissions =
|
||||
typeof client?.hasRequiredExtensionPermissions === "function" &&
|
||||
client.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
|
||||
if (shouldRequestExtensionPermissions(signer) && !hasCachedPermissions) {
|
||||
const permissionGate =
|
||||
typeof client?.ensureExtensionPermissionsGate === "function"
|
||||
? client.ensureExtensionPermissionsGate.bind(client)
|
||||
: typeof client?.ensureExtensionPermissions === "function"
|
||||
? client.ensureExtensionPermissions.bind(client)
|
||||
: null;
|
||||
|
||||
if (permissionGate) {
|
||||
permissionResult = await permissionGate(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
if (shouldRequestExtensionPermissions(signer)) {
|
||||
permissionResult = await client.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS
|
||||
);
|
||||
}
|
||||
if (permissionResult.ok) {
|
||||
try {
|
||||
|
||||
@@ -1530,12 +1530,7 @@ class WatchHistoryManager {
|
||||
|
||||
if (activeSigner) {
|
||||
if (this.deps.shouldRequestExtensionPermissions?.(activeSigner)) {
|
||||
const hasCachedPermissions =
|
||||
typeof this.deps.hasRequiredExtensionPermissions === "function" &&
|
||||
this.deps.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
if (!hasCachedPermissions) {
|
||||
await this.deps.ensureExtensionPermissions?.(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
await this.deps.ensureExtensionPermissions?.(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
|
||||
const encryptors = [];
|
||||
@@ -1868,12 +1863,7 @@ class WatchHistoryManager {
|
||||
const decryptSigner = canUseActiveSignerDecrypt ? signer : null;
|
||||
|
||||
if (decryptSigner && this.deps.shouldRequestExtensionPermissions?.(decryptSigner)) {
|
||||
const hasCachedPermissions =
|
||||
typeof this.deps.hasRequiredExtensionPermissions === "function" &&
|
||||
this.deps.hasRequiredExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
if (!hasCachedPermissions) {
|
||||
await this.deps.ensureExtensionPermissions?.(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
await this.deps.ensureExtensionPermissions?.(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
|
||||
devLogger.info("[nostr] Fetching watch history from relays.", {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
export {
|
||||
nostrClient,
|
||||
hasDefaultExtensionPermissions,
|
||||
requestDefaultExtensionPermissions,
|
||||
} from "./nostr/defaultClient.js";
|
||||
|
||||
|
||||
@@ -88,10 +88,6 @@ export function registerNostrClient(client, options = {}) {
|
||||
) {
|
||||
requestPermissionsDelegate = (...args) =>
|
||||
registeredNostrClient.ensureExtensionPermissions(...args);
|
||||
if (typeof registeredNostrClient.ensureExtensionPermissionsGate === "function") {
|
||||
requestPermissionsDelegate = (...args) =>
|
||||
registeredNostrClient.ensureExtensionPermissionsGate(...args);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { accessControl } from "../../accessControl.js";
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
|
||||
const PROVIDER_ID = "nip07";
|
||||
const PROVIDER_LABEL = "extension (nip-07)";
|
||||
@@ -75,16 +74,6 @@ export default {
|
||||
};
|
||||
|
||||
const result = await nostrClient.loginWithExtension({ ...options, validator });
|
||||
if (typeof nostrClient.ensureDmDecryptor === "function") {
|
||||
Promise.resolve()
|
||||
.then(() => nostrClient.ensureDmDecryptor())
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[auth:nip07] Failed to preload DM decryptor:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
const pubkey = normalizePubkey(result);
|
||||
const signer =
|
||||
result && typeof result === "object" && result.signer
|
||||
|
||||
@@ -27,9 +27,6 @@ const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
const DEFAULT_VERSION = 1;
|
||||
const DECRYPT_TIMEOUT_MS = 90000;
|
||||
const DECRYPT_RETRY_DELAY_MS = 10000;
|
||||
const FAST_RELAY_LIMIT = 3;
|
||||
const FAST_RELAY_TIMEOUT_MS = 4000;
|
||||
const FULL_RELAY_TIMEOUT_MS = 12000;
|
||||
|
||||
class TinyEventEmitter {
|
||||
constructor() {
|
||||
@@ -451,8 +448,7 @@ class HashtagPreferencesService {
|
||||
);
|
||||
}
|
||||
|
||||
async load(pubkey, options = {}) {
|
||||
const { skipFastRelays = false, isBackground = false, skipCache = false } = options;
|
||||
async load(pubkey) {
|
||||
const normalized = normalizeHexPubkey(pubkey);
|
||||
let wasLoadedForUser =
|
||||
this.activePubkey &&
|
||||
@@ -468,7 +464,7 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
// Try cache first
|
||||
if (!skipCache && this.loadFromCache(normalized)) {
|
||||
if (this.loadFromCache(normalized)) {
|
||||
wasLoadedForUser = true;
|
||||
}
|
||||
|
||||
@@ -527,43 +523,20 @@ class HashtagPreferencesService {
|
||||
// If we have cached data, we ask for updates since that timestamp.
|
||||
// If we have no data, we force a full fetch (since=0).
|
||||
const since = wasLoadedForUser ? Number(this.eventCreatedAt) || 0 : 0;
|
||||
const fastRelays = relays.slice(0, FAST_RELAY_LIMIT);
|
||||
const shouldUseFastRelays =
|
||||
!skipFastRelays && fastRelays.length > 0 && fastRelays.length < relays.length;
|
||||
const initialRelays = shouldUseFastRelays ? fastRelays : relays;
|
||||
const initialTimeoutMs = shouldUseFastRelays
|
||||
? FAST_RELAY_TIMEOUT_MS
|
||||
: FULL_RELAY_TIMEOUT_MS;
|
||||
|
||||
const fetchFromRelays = async (relayList, timeoutMs) => {
|
||||
const promises = kinds.map((kind) =>
|
||||
nostrClient.fetchListIncrementally({
|
||||
kind,
|
||||
pubkey: normalized,
|
||||
dTag: HASHTAG_IDENTIFIER,
|
||||
relayUrls: relayList,
|
||||
since,
|
||||
timeoutMs,
|
||||
}),
|
||||
);
|
||||
const promises = kinds.map((kind) =>
|
||||
nostrClient.fetchListIncrementally({
|
||||
kind,
|
||||
pubkey: normalized,
|
||||
dTag: HASHTAG_IDENTIFIER,
|
||||
relayUrls: relays,
|
||||
since,
|
||||
timeoutMs: 12000,
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
return results.flat();
|
||||
};
|
||||
|
||||
events = await fetchFromRelays(initialRelays, initialTimeoutMs);
|
||||
if (!events.length && shouldUseFastRelays && !isBackground) {
|
||||
devLogger.log(
|
||||
`${LOG_PREFIX} Fast relay fetch returned no events; continuing in background.`,
|
||||
);
|
||||
this.load(normalized, {
|
||||
skipFastRelays: true,
|
||||
isBackground: true,
|
||||
skipCache: true,
|
||||
}).catch((error) => {
|
||||
devLogger.warn(`${LOG_PREFIX} Background refresh failed`, error);
|
||||
});
|
||||
}
|
||||
const results = await Promise.all(promises);
|
||||
events = results.flat();
|
||||
|
||||
} catch (error) {
|
||||
fetchError = error;
|
||||
@@ -744,14 +717,7 @@ class HashtagPreferencesService {
|
||||
(!signerHasNip04 && requiresNip04)
|
||||
) {
|
||||
try {
|
||||
const permissionMethods = [];
|
||||
if (requiresNip04) {
|
||||
permissionMethods.push("nip04.decrypt");
|
||||
}
|
||||
if (requiresNip44) {
|
||||
permissionMethods.push("nip44.decrypt", "nip44.v2.decrypt");
|
||||
}
|
||||
await requestDefaultExtensionPermissions(permissionMethods);
|
||||
await requestDefaultExtensionPermissions();
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
`${LOG_PREFIX} Extension permissions request failed while loading preferences`,
|
||||
@@ -954,15 +920,7 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
if (signer.type === "extension") {
|
||||
const permissionMethods = ["sign_event"];
|
||||
if (typeof signer.nip04Encrypt === "function") {
|
||||
permissionMethods.push("nip04.encrypt");
|
||||
}
|
||||
if (typeof signer.nip44Encrypt === "function") {
|
||||
permissionMethods.push("nip44.encrypt", "nip44.v2.encrypt");
|
||||
}
|
||||
const permissionResult =
|
||||
await requestDefaultExtensionPermissions(permissionMethods);
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
const error = new Error(
|
||||
"The active signer must allow encryption and signing before publishing preferences.",
|
||||
|
||||
@@ -958,24 +958,6 @@ export class NostrService {
|
||||
...options,
|
||||
limit: resolvedLimit,
|
||||
decryptLimit: resolvedDecryptLimit,
|
||||
onPermissionRequest: (detail = {}) => {
|
||||
const status = detail?.status;
|
||||
if (status === "decrypting") {
|
||||
this.emit("directMessages:status", {
|
||||
status,
|
||||
message: "Decrypting direct messages…",
|
||||
methods: detail?.methods || [],
|
||||
eventId: detail?.eventId || null,
|
||||
});
|
||||
} else if (status === "ready") {
|
||||
this.emit("directMessages:status", { status: "ready" });
|
||||
} else if (status === "error") {
|
||||
this.emit("directMessages:status", {
|
||||
status: "error",
|
||||
error: detail?.error || null,
|
||||
});
|
||||
}
|
||||
},
|
||||
onMessage: (message) => {
|
||||
this.applyDirectMessage(message, { reason: "load-incremental" });
|
||||
},
|
||||
@@ -1095,24 +1077,6 @@ export class NostrService {
|
||||
{
|
||||
relays: resolvedRelays,
|
||||
onEvent: handlers.onEvent,
|
||||
onPermissionRequest: (detail = {}) => {
|
||||
const status = detail?.status;
|
||||
if (status === "decrypting") {
|
||||
this.emit("directMessages:status", {
|
||||
status,
|
||||
message: "Decrypting direct messages…",
|
||||
methods: detail?.methods || [],
|
||||
eventId: detail?.eventId || null,
|
||||
});
|
||||
} else if (status === "ready") {
|
||||
this.emit("directMessages:status", { status: "ready" });
|
||||
} else if (status === "error") {
|
||||
this.emit("directMessages:status", {
|
||||
status: "error",
|
||||
error: detail?.error || null,
|
||||
});
|
||||
}
|
||||
},
|
||||
onMessage: (message, context = {}) => {
|
||||
this.applyDirectMessage(message, {
|
||||
reason: "subscription",
|
||||
|
||||
+50
-137
@@ -1,7 +1,6 @@
|
||||
// js/subscriptions.js
|
||||
import {
|
||||
getActiveSigner,
|
||||
hasDefaultExtensionPermissions,
|
||||
nostrClient,
|
||||
requestDefaultExtensionPermissions,
|
||||
} from "./nostrClientFacade.js";
|
||||
@@ -38,9 +37,6 @@ const SUBSCRIPTION_SET_KIND =
|
||||
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
|
||||
const DECRYPT_TIMEOUT_MS = 90000;
|
||||
const DECRYPT_RETRY_DELAY_MS = 10000;
|
||||
const FAST_RELAY_LIMIT = 3;
|
||||
const FAST_RELAY_TIMEOUT_MS = 4000;
|
||||
const FULL_RELAY_TIMEOUT_MS = 12000;
|
||||
|
||||
function normalizeHexPubkey(value) {
|
||||
if (typeof value !== "string") {
|
||||
@@ -360,28 +356,6 @@ class SubscriptionsManager {
|
||||
});
|
||||
}
|
||||
|
||||
emitStatus(detail = {}) {
|
||||
this.emitter.emit("status", detail);
|
||||
|
||||
if (detail?.status !== "loading") {
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
typeof detail?.message === "string" && detail.message.trim()
|
||||
? detail.message.trim()
|
||||
: "Decrypting subscriptions…";
|
||||
const container =
|
||||
this.lastContainerId &&
|
||||
typeof document !== "undefined" &&
|
||||
document.getElementById
|
||||
? document.getElementById(this.lastContainerId)
|
||||
: null;
|
||||
if (container) {
|
||||
container.innerHTML = getSidebarLoadingMarkup(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the subscription list from kind=30000 (d="subscriptions").
|
||||
*/
|
||||
@@ -451,16 +425,14 @@ class SubscriptionsManager {
|
||||
);
|
||||
}
|
||||
|
||||
async updateFromRelays(userPubkey, options = {}) {
|
||||
async updateFromRelays(userPubkey) {
|
||||
if (!userPubkey) return;
|
||||
|
||||
try {
|
||||
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
|
||||
const { relayUrlsOverride, skipFastRelays = false, isBackground = false } = options;
|
||||
|
||||
const readRelays = relayManager.getReadRelayUrls();
|
||||
const fallbackRelayUrls = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
const relayUrls = relayUrlsOverride?.length ? relayUrlsOverride : fallbackRelayUrls;
|
||||
const relayUrls = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
if (!relayUrls.length) {
|
||||
devLogger.warn(
|
||||
"[SubscriptionsManager] No relay URLs available while loading subscriptions.",
|
||||
@@ -468,75 +440,45 @@ class SubscriptionsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const fastRelayUrls = relayUrls.slice(0, FAST_RELAY_LIMIT);
|
||||
const shouldUseFastRelays =
|
||||
!skipFastRelays && fastRelayUrls.length > 0 && fastRelayUrls.length < relayUrls.length;
|
||||
const initialRelayUrls = shouldUseFastRelays ? fastRelayUrls : relayUrls;
|
||||
const initialTimeoutMs = shouldUseFastRelays
|
||||
? FAST_RELAY_TIMEOUT_MS
|
||||
: FULL_RELAY_TIMEOUT_MS;
|
||||
|
||||
// Use incremental fetch helper
|
||||
const cachedSnapshot = parseCachedSubscriptionSnapshot(
|
||||
profileCache.getProfileData(normalizedUserPubkey, "subscriptions"),
|
||||
);
|
||||
const shouldForceFullFetch = !cachedSnapshot.hasSnapshot;
|
||||
let events = await nostrClient.fetchListIncrementally({
|
||||
kind: SUBSCRIPTION_SET_KIND,
|
||||
pubkey: normalizedUserPubkey,
|
||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
relayUrls,
|
||||
since: shouldForceFullFetch ? 0 : undefined,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
|
||||
const normalizedSessionActorPubkey = normalizeNostrPubkey(
|
||||
nostrClient?.sessionActor?.pubkey,
|
||||
);
|
||||
const shouldFetchSessionActor =
|
||||
normalizedSessionActorPubkey &&
|
||||
normalizedSessionActorPubkey !== normalizedUserPubkey;
|
||||
|
||||
const fetchFromRelays = async (relayList, timeoutMs) => {
|
||||
let events = await nostrClient.fetchListIncrementally({
|
||||
kind: SUBSCRIPTION_SET_KIND,
|
||||
pubkey: normalizedUserPubkey,
|
||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
relayUrls: relayList,
|
||||
since: shouldForceFullFetch ? 0 : undefined,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
if (shouldFetchSessionActor) {
|
||||
const sessionCachedSnapshot = parseCachedSubscriptionSnapshot(
|
||||
profileCache.getProfileData(
|
||||
normalizedSessionActorPubkey,
|
||||
"subscriptions",
|
||||
),
|
||||
);
|
||||
const shouldForceSessionFetch = !sessionCachedSnapshot.hasSnapshot;
|
||||
const sessionEvents = await nostrClient.fetchListIncrementally({
|
||||
kind: SUBSCRIPTION_SET_KIND,
|
||||
pubkey: normalizedSessionActorPubkey,
|
||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
relayUrls: relayList,
|
||||
since: shouldForceSessionFetch ? 0 : undefined,
|
||||
timeoutMs,
|
||||
});
|
||||
if (sessionEvents.length) {
|
||||
events = events.concat(sessionEvents);
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
};
|
||||
|
||||
let events = await fetchFromRelays(initialRelayUrls, initialTimeoutMs);
|
||||
if (!events.length && shouldUseFastRelays && !isBackground) {
|
||||
devLogger.log(
|
||||
"[SubscriptionsManager] Fast relay fetch returned no events; continuing in background.",
|
||||
if (shouldFetchSessionActor) {
|
||||
const sessionCachedSnapshot = parseCachedSubscriptionSnapshot(
|
||||
profileCache.getProfileData(
|
||||
normalizedSessionActorPubkey,
|
||||
"subscriptions",
|
||||
),
|
||||
);
|
||||
this.updateFromRelays(normalizedUserPubkey, {
|
||||
relayUrlsOverride: relayUrls,
|
||||
skipFastRelays: true,
|
||||
isBackground: true,
|
||||
}).catch((error) => {
|
||||
devLogger.warn(
|
||||
"[SubscriptionsManager] Background relay refresh failed:",
|
||||
error,
|
||||
);
|
||||
const shouldForceSessionFetch = !sessionCachedSnapshot.hasSnapshot;
|
||||
const sessionEvents = await nostrClient.fetchListIncrementally({
|
||||
kind: SUBSCRIPTION_SET_KIND,
|
||||
pubkey: normalizedSessionActorPubkey,
|
||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
relayUrls,
|
||||
since: shouldForceSessionFetch ? 0 : undefined,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
if (sessionEvents.length) {
|
||||
events = events.concat(sessionEvents);
|
||||
}
|
||||
}
|
||||
|
||||
const mergedEvents = [];
|
||||
@@ -808,38 +750,21 @@ class SubscriptionsManager {
|
||||
(!signerHasNip04 && !windowHasNip04 && requiresNip04)
|
||||
) {
|
||||
try {
|
||||
const permissionMethods = [];
|
||||
if (requiresNip04) {
|
||||
permissionMethods.push("nip04.decrypt");
|
||||
}
|
||||
if (requiresNip44) {
|
||||
permissionMethods.push("nip44.decrypt", "nip44.v2.decrypt");
|
||||
}
|
||||
const hasCachedPermissions = hasDefaultExtensionPermissions(permissionMethods);
|
||||
if (!hasCachedPermissions) {
|
||||
this.emitStatus({
|
||||
status: "loading",
|
||||
message: "Decrypting subscriptions…",
|
||||
});
|
||||
const permissionResult =
|
||||
await requestDefaultExtensionPermissions(permissionMethods);
|
||||
if (!permissionResult?.ok) {
|
||||
const error =
|
||||
permissionResult?.error instanceof Error
|
||||
? permissionResult.error
|
||||
: new Error(
|
||||
"Extension permissions denied while decrypting subscriptions."
|
||||
);
|
||||
error.code = "nostr-permission-denied";
|
||||
return { ok: false, error };
|
||||
}
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
const error =
|
||||
permissionResult?.error instanceof Error
|
||||
? permissionResult.error
|
||||
: new Error(
|
||||
"Extension permissions denied while decrypting subscriptions."
|
||||
);
|
||||
error.code = "nostr-permission-denied";
|
||||
return { ok: false, error };
|
||||
}
|
||||
} catch (error) {
|
||||
const wrapped = error instanceof Error ? error : new Error(String(error));
|
||||
wrapped.code = "nostr-permission-error";
|
||||
return { ok: false, error: wrapped };
|
||||
} finally {
|
||||
this.emitStatus({ status: "idle" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1054,30 +979,18 @@ class SubscriptionsManager {
|
||||
}
|
||||
|
||||
if (signer.type === "extension") {
|
||||
const permissionMethods = ["sign_event"];
|
||||
if (typeof signer.nip04Encrypt === "function") {
|
||||
permissionMethods.push("nip04.encrypt");
|
||||
}
|
||||
if (typeof signer.nip44Encrypt === "function") {
|
||||
permissionMethods.push("nip44.encrypt", "nip44.v2.encrypt");
|
||||
}
|
||||
const hasCachedPermissions =
|
||||
hasDefaultExtensionPermissions(permissionMethods);
|
||||
if (!hasCachedPermissions) {
|
||||
const permissionResult =
|
||||
await requestDefaultExtensionPermissions(permissionMethods);
|
||||
if (!permissionResult.ok) {
|
||||
userLogger.warn(
|
||||
"[SubscriptionsManager] Signer permissions denied while updating subscriptions.",
|
||||
permissionResult.error,
|
||||
);
|
||||
const error = new Error(
|
||||
"The active signer must allow encryption and signing before updating subscriptions.",
|
||||
);
|
||||
error.code = "extension-permission-denied";
|
||||
error.cause = permissionResult.error;
|
||||
throw error;
|
||||
}
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult.ok) {
|
||||
userLogger.warn(
|
||||
"[SubscriptionsManager] Signer permissions denied while updating subscriptions.",
|
||||
permissionResult.error,
|
||||
);
|
||||
const error = new Error(
|
||||
"The active signer must allow encryption and signing before updating subscriptions.",
|
||||
);
|
||||
error.code = "extension-permission-denied";
|
||||
error.cause = permissionResult.error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -940,8 +940,7 @@ export class UploadModal {
|
||||
|
||||
try {
|
||||
if (signer?.type === "extension") {
|
||||
const permissionResult =
|
||||
await requestDefaultExtensionPermissions(["nip04.decrypt"]);
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
alert("Extension permissions are required to unlock storage.");
|
||||
return;
|
||||
|
||||
+32
-258
@@ -1270,8 +1270,6 @@ export class ProfileModalController {
|
||||
this.blockListEmpty = null;
|
||||
this.blockListStatus = null;
|
||||
this.blockListLoadingState = "idle";
|
||||
this.subscriptionsStatus = null;
|
||||
this.subscriptionsLoadingState = "idle";
|
||||
this.blockInput = null;
|
||||
this.addBlockedButton = null;
|
||||
this.profileBlockedList = null;
|
||||
@@ -1417,9 +1415,6 @@ export class ProfileModalController {
|
||||
this.subscriptionsService.on("change", () => {
|
||||
void this.populateSubscriptionsList();
|
||||
});
|
||||
this.subscriptionsService.on("status", (detail) => {
|
||||
this.handleSubscriptionsStatus(detail);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1429,9 +1424,6 @@ export class ProfileModalController {
|
||||
this.services.userBlocks.on("change", () => {
|
||||
this.populateBlockedList();
|
||||
});
|
||||
this.services.userBlocks.on("status", (detail) => {
|
||||
this.handleBlockListStatus(detail);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1592,10 +1584,6 @@ export class ProfileModalController {
|
||||
|
||||
this.blockList = document.getElementById("blockedList") || null;
|
||||
this.blockListEmpty = document.getElementById("blockedEmpty") || null;
|
||||
this.blockListEmptyDefaultText =
|
||||
this.blockListEmpty instanceof HTMLElement
|
||||
? this.blockListEmpty.textContent.trim()
|
||||
: "";
|
||||
this.blockInput = document.getElementById("blockedInput") || null;
|
||||
this.addBlockedButton = document.getElementById("addBlockedBtn") || null;
|
||||
|
||||
@@ -1965,9 +1953,6 @@ export class ProfileModalController {
|
||||
subscribe("directMessages:updated", (detail) => {
|
||||
this.handleDirectMessagesUpdated(detail);
|
||||
});
|
||||
subscribe("directMessages:status", (detail) => {
|
||||
this.handleDirectMessagesStatus(detail);
|
||||
});
|
||||
subscribe("directMessages:cleared", () => {
|
||||
this.handleDirectMessagesCleared();
|
||||
});
|
||||
@@ -5033,38 +5018,6 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
handleDirectMessagesStatus(detail = {}) {
|
||||
const status = typeof detail?.status === "string" ? detail.status : "";
|
||||
if (status === "decrypting") {
|
||||
this.setMessagesLoadingState("loading", {
|
||||
message:
|
||||
typeof detail?.message === "string" && detail.message.trim()
|
||||
? detail.message.trim()
|
||||
: "Decrypting direct messages…",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === "error") {
|
||||
this.setMessagesLoadingState("error", {
|
||||
message:
|
||||
"Unable to decrypt direct messages. Check your extension permissions.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === "ready" && this.messagesLoadingState === "loading") {
|
||||
const actor = this.resolveActiveDmActor();
|
||||
if (!actor) {
|
||||
this.setMessagesLoadingState("unauthenticated");
|
||||
} else if (!this.directMessagesCache.length) {
|
||||
this.setMessagesLoadingState("empty");
|
||||
} else {
|
||||
this.setMessagesLoadingState("ready");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDirectMessagesCleared() {
|
||||
if (this.activeMessagesRequest) {
|
||||
return;
|
||||
@@ -7440,144 +7393,6 @@ export class ProfileModalController {
|
||||
}
|
||||
}
|
||||
|
||||
handleBlockListStatus(detail = {}) {
|
||||
const status = typeof detail?.status === "string" ? detail.status : "";
|
||||
const message =
|
||||
typeof detail?.message === "string" && detail.message.trim()
|
||||
? detail.message.trim()
|
||||
: "";
|
||||
|
||||
if (status === "loading") {
|
||||
this.setBlockListLoadingState("loading", { message });
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === "error") {
|
||||
this.setBlockListLoadingState("error", { message });
|
||||
return;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
this.setBlockListLoadingState("idle");
|
||||
}
|
||||
}
|
||||
|
||||
ensureSubscriptionsStatusElement() {
|
||||
if (this.subscriptionsStatus instanceof HTMLElement) {
|
||||
return this.subscriptionsStatus;
|
||||
}
|
||||
|
||||
const anchor =
|
||||
this.subscriptionList instanceof HTMLElement
|
||||
? this.subscriptionList
|
||||
: this.subscriptionListEmpty instanceof HTMLElement
|
||||
? this.subscriptionListEmpty
|
||||
: null;
|
||||
|
||||
if (!anchor || !(anchor.parentElement instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existing = anchor.parentElement.querySelector(
|
||||
'[data-role="subscriptions-status"]',
|
||||
);
|
||||
if (existing instanceof HTMLElement) {
|
||||
this.subscriptionsStatus = existing;
|
||||
return existing;
|
||||
}
|
||||
|
||||
const status = document.createElement("div");
|
||||
status.dataset.role = "subscriptions-status";
|
||||
status.className = "mt-4 flex items-center gap-3 text-sm text-muted hidden";
|
||||
status.setAttribute("role", "status");
|
||||
status.setAttribute("aria-live", "polite");
|
||||
|
||||
if (this.subscriptionList instanceof HTMLElement) {
|
||||
anchor.parentElement.insertBefore(status, this.subscriptionList);
|
||||
} else {
|
||||
anchor.parentElement.appendChild(status);
|
||||
}
|
||||
|
||||
this.subscriptionsStatus = status;
|
||||
return status;
|
||||
}
|
||||
|
||||
setSubscriptionsLoadingState(state = "idle", options = {}) {
|
||||
const statusEl = this.ensureSubscriptionsStatusElement();
|
||||
if (!statusEl) {
|
||||
this.subscriptionsLoadingState = state;
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
typeof options.message === "string" && options.message.trim()
|
||||
? options.message.trim()
|
||||
: "";
|
||||
|
||||
statusEl.innerHTML = "";
|
||||
statusEl.classList.remove("text-status-warning");
|
||||
statusEl.classList.add("text-muted");
|
||||
statusEl.classList.add("hidden");
|
||||
|
||||
this.subscriptionsLoadingState = state;
|
||||
|
||||
if (state === "loading") {
|
||||
if (this.subscriptionListEmpty instanceof HTMLElement) {
|
||||
this.subscriptionListEmpty.classList.add("hidden");
|
||||
}
|
||||
|
||||
const spinner = document.createElement("span");
|
||||
spinner.className = "status-spinner status-spinner--inline";
|
||||
spinner.setAttribute("aria-hidden", "true");
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent = message || "Loading subscriptions…";
|
||||
|
||||
statusEl.appendChild(spinner);
|
||||
statusEl.appendChild(text);
|
||||
statusEl.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === "error") {
|
||||
statusEl.classList.remove("text-muted");
|
||||
statusEl.classList.add("text-status-warning");
|
||||
|
||||
if (this.subscriptionListEmpty instanceof HTMLElement) {
|
||||
this.subscriptionListEmpty.classList.add("hidden");
|
||||
}
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent =
|
||||
message || "Subscriptions may be out of date. Try again later.";
|
||||
|
||||
statusEl.appendChild(text);
|
||||
statusEl.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
handleSubscriptionsStatus(detail = {}) {
|
||||
const status = typeof detail?.status === "string" ? detail.status : "";
|
||||
const message =
|
||||
typeof detail?.message === "string" && detail.message.trim()
|
||||
? detail.message.trim()
|
||||
: "";
|
||||
|
||||
if (status === "loading") {
|
||||
this.setSubscriptionsLoadingState("loading", { message });
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === "error") {
|
||||
this.setSubscriptionsLoadingState("error", { message });
|
||||
return;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
this.setSubscriptionsLoadingState("idle");
|
||||
}
|
||||
}
|
||||
|
||||
normalizeHashtagTag(value) {
|
||||
return normalizeHashtag(value);
|
||||
}
|
||||
@@ -8039,10 +7854,10 @@ export class ProfileModalController {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasExplicitList = Array.isArray(blocked);
|
||||
const sourceEntries = hasExplicitList
|
||||
? blocked
|
||||
: this.services.userBlocks.getBlockedPubkeys();
|
||||
const sourceEntries =
|
||||
Array.isArray(blocked) && blocked.length
|
||||
? blocked
|
||||
: this.services.userBlocks.getBlockedPubkeys();
|
||||
|
||||
const normalizedEntries = [];
|
||||
const pushEntry = (hex, label) => {
|
||||
@@ -8115,13 +7930,7 @@ export class ProfileModalController {
|
||||
|
||||
this.blockList.innerHTML = "";
|
||||
|
||||
const blocksLoaded = this.services.userBlocks?.loaded === true;
|
||||
if (!deduped.length) {
|
||||
if (this.blockListLoadingState === "loading" && !blocksLoaded) {
|
||||
this.blockListEmpty.classList.add("hidden");
|
||||
this.blockList.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
this.blockListEmpty.classList.remove("hidden");
|
||||
this.blockList.classList.add("hidden");
|
||||
if (this.blockListLoadingState === "loading") {
|
||||
@@ -8329,18 +8138,9 @@ export class ProfileModalController {
|
||||
|
||||
this.subscriptionList.innerHTML = "";
|
||||
|
||||
const subscriptionsLoaded = service.loaded === true;
|
||||
if (!deduped.length) {
|
||||
if (this.subscriptionsLoadingState === "loading" && !subscriptionsLoaded) {
|
||||
this.subscriptionListEmpty.classList.add("hidden");
|
||||
this.subscriptionList.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
this.subscriptionListEmpty.classList.remove("hidden");
|
||||
this.subscriptionList.classList.add("hidden");
|
||||
if (this.subscriptionsLoadingState === "loading") {
|
||||
this.setSubscriptionsLoadingState("idle");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8424,10 +8224,6 @@ export class ProfileModalController {
|
||||
this.subscriptionList.appendChild(item);
|
||||
});
|
||||
|
||||
if (this.subscriptionsLoadingState === "loading") {
|
||||
this.setSubscriptionsLoadingState("idle");
|
||||
}
|
||||
|
||||
if (
|
||||
entriesNeedingFetch.size &&
|
||||
typeof this.services.batchFetchProfiles === "function"
|
||||
@@ -8455,10 +8251,6 @@ export class ProfileModalController {
|
||||
if (this.subscriptionListEmpty instanceof HTMLElement) {
|
||||
this.subscriptionListEmpty.classList.remove("hidden");
|
||||
}
|
||||
|
||||
if (this.subscriptionsLoadingState === "loading") {
|
||||
this.setSubscriptionsLoadingState("idle");
|
||||
}
|
||||
}
|
||||
|
||||
async handleUnsubscribeFromCreator(candidate) {
|
||||
@@ -12714,54 +12506,40 @@ export class ProfileModalController {
|
||||
|
||||
if (hasBlockHydrator) {
|
||||
const activeHex = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (!activeHex) {
|
||||
this.setBlockListLoadingState("idle");
|
||||
if (this.blockListEmpty instanceof HTMLElement) {
|
||||
this.blockListEmpty.textContent =
|
||||
"Connect a Nostr signer to load your mute list.";
|
||||
}
|
||||
this.populateBlockedList([]);
|
||||
} else {
|
||||
if (this.blockListEmpty instanceof HTMLElement) {
|
||||
this.blockListEmpty.textContent =
|
||||
this.blockListEmptyDefaultText ||
|
||||
"You haven’t blocked any creators yet.";
|
||||
}
|
||||
backgroundTasks.push(
|
||||
Promise.resolve()
|
||||
.then(() => this.services.userBlocks.ensureLoaded(activeHex))
|
||||
.then(() => {
|
||||
try {
|
||||
this.populateBlockedList();
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
"Failed to render blocked creators after hydration:",
|
||||
error,
|
||||
);
|
||||
this.setBlockListLoadingState("error", {
|
||||
message: "Blocked creators may be out of date. Try again later.",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
backgroundTasks.push(
|
||||
Promise.resolve()
|
||||
.then(() => this.services.userBlocks.ensureLoaded(activeHex))
|
||||
.then(() => {
|
||||
try {
|
||||
this.populateBlockedList();
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
"Failed to refresh user block list while opening profile modal:",
|
||||
"Failed to render blocked creators after hydration:",
|
||||
error,
|
||||
);
|
||||
this.setBlockListLoadingState("error", {
|
||||
message: "Blocked creators may be out of date. Try again later.",
|
||||
});
|
||||
try {
|
||||
this.populateBlockedList();
|
||||
} catch (populateError) {
|
||||
userLogger.warn(
|
||||
"Failed to render blocked creators after hydration failure:",
|
||||
populateError,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
userLogger.warn(
|
||||
"Failed to refresh user block list while opening profile modal:",
|
||||
error,
|
||||
);
|
||||
this.setBlockListLoadingState("error", {
|
||||
message: "Blocked creators may be out of date. Try again later.",
|
||||
});
|
||||
try {
|
||||
this.populateBlockedList();
|
||||
} catch (populateError) {
|
||||
userLogger.warn(
|
||||
"Failed to render blocked creators after hydration failure:",
|
||||
populateError,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (backgroundTasks.length) {
|
||||
@@ -12969,8 +12747,6 @@ export class ProfileModalController {
|
||||
|
||||
// Trigger aggressive parallel fetches
|
||||
if (activePubkey) {
|
||||
this.setBlockListLoadingState("loading");
|
||||
this.setSubscriptionsLoadingState("loading");
|
||||
if (this.services.userBlocks) {
|
||||
this.services.userBlocks.loadBlocks(activePubkey).catch(noop);
|
||||
}
|
||||
@@ -13036,8 +12812,6 @@ export class ProfileModalController {
|
||||
|
||||
this.profileSwitcherSelectionPubkey = null;
|
||||
this.renderSavedProfiles();
|
||||
this.setBlockListLoadingState("idle");
|
||||
this.setSubscriptionsLoadingState("idle");
|
||||
|
||||
try {
|
||||
await this.refreshAdminPaneState();
|
||||
|
||||
@@ -323,10 +323,6 @@ export default class UrlHealthController {
|
||||
return { outcome: "invalid" };
|
||||
}
|
||||
|
||||
if (trimmed.toLowerCase().endsWith(".torrent")) {
|
||||
return { outcome: "invalid" };
|
||||
}
|
||||
|
||||
const confirmPlayable = options?.confirmPlayable === true;
|
||||
const defaultTimeout = this.constants.URL_PROBE_TIMEOUT_MS || 5000;
|
||||
const urlHealthConstants = this.constants.urlHealthConstants || {};
|
||||
|
||||
+26
-75
@@ -73,7 +73,7 @@ export const USER_BLOCK_EVENTS = Object.freeze({
|
||||
|
||||
const FAST_BLOCKLIST_RELAY_LIMIT = 3;
|
||||
const FAST_BLOCKLIST_TIMEOUT_MS = 2500;
|
||||
const FULL_BLOCKLIST_TIMEOUT_MS = 12000;
|
||||
const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000;
|
||||
const DECRYPT_TIMEOUT_MS = 90000;
|
||||
const DECRYPT_RETRY_DELAY_MS = 10000;
|
||||
|
||||
@@ -718,12 +718,12 @@ class UserBlockListManager {
|
||||
}
|
||||
|
||||
async ensureLoaded(userPubkey) {
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
if (!normalized) {
|
||||
if (this.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.loaded && this.activePubkey === normalized) {
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -751,39 +751,28 @@ class UserBlockListManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.isBackground) {
|
||||
try {
|
||||
statusCallback?.(detail);
|
||||
} catch (callbackError) {
|
||||
userLogger.warn(
|
||||
"[UserBlockList] statusCallback threw while emitting status",
|
||||
callbackError,
|
||||
);
|
||||
}
|
||||
try {
|
||||
statusCallback?.(detail);
|
||||
} catch (callbackError) {
|
||||
userLogger.warn(
|
||||
"[UserBlockList] statusCallback threw while emitting status",
|
||||
callbackError,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
this.emitter.emit(USER_BLOCK_EVENTS.STATUS, detail);
|
||||
} catch (emitterError) {
|
||||
userLogger.warn(
|
||||
"[UserBlockList] Failed to dispatch status event",
|
||||
emitterError,
|
||||
);
|
||||
}
|
||||
try {
|
||||
this.emitter.emit(USER_BLOCK_EVENTS.STATUS, detail);
|
||||
} catch (emitterError) {
|
||||
userLogger.warn(
|
||||
"[UserBlockList] Failed to dispatch status event",
|
||||
emitterError,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const readRelays = relayManager.getReadRelayUrls();
|
||||
const relays = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
const fastRelays = relays.slice(0, FAST_BLOCKLIST_RELAY_LIMIT);
|
||||
const shouldUseFastRelays =
|
||||
!options.skipFastRelays &&
|
||||
fastRelays.length > 0 &&
|
||||
fastRelays.length < relays.length;
|
||||
const initialRelays = shouldUseFastRelays ? fastRelays : relays;
|
||||
const initialTimeoutMs = shouldUseFastRelays
|
||||
? FAST_BLOCKLIST_TIMEOUT_MS
|
||||
: FULL_BLOCKLIST_TIMEOUT_MS;
|
||||
emitStatus({ status: "loading", relays: initialRelays });
|
||||
emitStatus({ status: "loading", relays });
|
||||
|
||||
const localBlocks = this._loadLocal(normalized);
|
||||
const hasLocalData = !!localBlocks;
|
||||
@@ -836,20 +825,7 @@ class UserBlockListManager {
|
||||
let permissionError = null;
|
||||
if ((!signerHasNip44 && requiresNip44) || (!signerHasNip04 && requiresNip04)) {
|
||||
try {
|
||||
const permissionMethods = [];
|
||||
if (requiresNip04) {
|
||||
permissionMethods.push("nip04.decrypt");
|
||||
}
|
||||
if (requiresNip44) {
|
||||
permissionMethods.push("nip44.decrypt", "nip44.v2.decrypt");
|
||||
}
|
||||
emitStatus({
|
||||
status: "loading",
|
||||
message: "Decrypting blocked creators…",
|
||||
reason: "permissions",
|
||||
});
|
||||
const permissionResult =
|
||||
await requestDefaultExtensionPermissions(permissionMethods);
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
permissionError =
|
||||
permissionResult?.error instanceof Error
|
||||
@@ -860,8 +836,6 @@ class UserBlockListManager {
|
||||
}
|
||||
} catch (error) {
|
||||
permissionError = error instanceof Error ? error : new Error(String(error));
|
||||
} finally {
|
||||
emitStatus({ status: "settled", reason: "permissions" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1217,17 +1191,17 @@ class UserBlockListManager {
|
||||
nostrClient.fetchListIncrementally({
|
||||
kind: 10000,
|
||||
pubkey: normalized,
|
||||
relayUrls: initialRelays,
|
||||
relayUrls: relays,
|
||||
since: fetchSince,
|
||||
timeoutMs: initialTimeoutMs,
|
||||
timeoutMs: 12000,
|
||||
}),
|
||||
nostrClient.fetchListIncrementally({
|
||||
kind: 10000,
|
||||
pubkey: normalized,
|
||||
dTag: BLOCK_LIST_IDENTIFIER,
|
||||
relayUrls: initialRelays,
|
||||
relayUrls: relays,
|
||||
since: fetchSince,
|
||||
timeoutMs: initialTimeoutMs,
|
||||
timeoutMs: 12000,
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -1235,21 +1209,6 @@ class UserBlockListManager {
|
||||
|
||||
// If combined is empty, check if we have loaded state
|
||||
if (!combined.length) {
|
||||
if (shouldUseFastRelays && !options.isBackground) {
|
||||
devLogger.log(
|
||||
"[UserBlockList] Fast relay fetch returned no events; continuing in background.",
|
||||
);
|
||||
this.loadBlocks(normalized, {
|
||||
...options,
|
||||
skipFastRelays: true,
|
||||
isBackground: true,
|
||||
}).catch((error) => {
|
||||
devLogger.warn(
|
||||
"[UserBlockList] Background relay refresh failed:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
if (!this.loaded) {
|
||||
// Assuming empty if we have no prior state
|
||||
await applyEvents([], { source: "incremental" });
|
||||
@@ -1830,15 +1789,7 @@ class UserBlockListManager {
|
||||
}
|
||||
|
||||
if (signer.type === "extension") {
|
||||
const permissionMethods = ["sign_event"];
|
||||
if (typeof signer.nip04Encrypt === "function") {
|
||||
permissionMethods.push("nip04.encrypt");
|
||||
}
|
||||
if (typeof signer.nip44Encrypt === "function") {
|
||||
permissionMethods.push("nip44.encrypt", "nip44.v2.encrypt");
|
||||
}
|
||||
const permissionResult =
|
||||
await requestDefaultExtensionPermissions(permissionMethods);
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult.ok) {
|
||||
userLogger.warn(
|
||||
"[UserBlockList] Signer permissions denied while updating the block list.",
|
||||
|
||||
Reference in new issue
Block a user