chore(ai): add automated PR review report

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-01-28 23:49:33 +00:00
1 parent c3f96c8819
commit cfbbf78941
30 files changed
+616 -1765

No files matched your search

+44 -98
View File
@@ -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();
-2
View File
@@ -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
View File
@@ -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) {
+4 -15
View File
@@ -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) {
+1 -7
View File
@@ -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,
+8 -30
View File
@@ -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) {
+24 -44
View File
@@ -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,
+4 -15
View File
@@ -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
View File
@@ -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 {
+2 -12
View File
@@ -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.", {
-1
View File
@@ -7,7 +7,6 @@
export {
nostrClient,
hasDefaultExtensionPermissions,
requestDefaultExtensionPermissions,
} from "./nostr/defaultClient.js";
-4
View File
@@ -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;
}
-11
View File
@@ -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
+16 -58
View File
@@ -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.",
-36
View File
@@ -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
View File
@@ -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;
}
}
+1 -2
View File
@@ -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
View File
@@ -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();
-4
View File
@@ -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
View File
@@ -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.",