From 1403d2d4cceff55c94510ef7fc9ad1660235faaf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 05:42:24 +0000 Subject: [PATCH] fix: handle permission-required errors in hashtag preferences background loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When hashtag preferences decryption fails with "permission-required" during background loading (allowPermissionPrompt=false), the service silently returned without setting loaded state, scheduling a retry, or emitting a change event. This left the UI stuck showing empty hashtag lists. The subscriptions module already handled this case correctly. Also fix error scheme mapping in Promise.any() catch blocks for both hashtag preferences and subscriptions — the index into the original order array was wrong after filtering out deduplicated scheme families. https://claude.ai/code/session_016xYxLQ1U2XVVY5rFuCRKfc --- js/services/hashtagPreferencesService.js | 14 +++++++++++++- js/subscriptions.js | 4 +++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/js/services/hashtagPreferencesService.js b/js/services/hashtagPreferencesService.js index 63a4586f..5fdfb85a 100644 --- a/js/services/hashtagPreferencesService.js +++ b/js/services/hashtagPreferencesService.js @@ -829,6 +829,16 @@ class HashtagPreferencesService { !allowPermissionPrompt && decryptResult.error?.code === "hashtag-preferences-permission-required" ) { + if (!wasLoadedForUser && !this.loaded) { + this.loaded = true; + } + this.scheduleDecryptRetry(normalized, decryptResult.error, { + allowPermissionPrompt: true, + }); + if (wasBackgroundLoading) { + this.backgroundLoading = false; + this.emitChange("background-loaded", { background: false }); + } return; } @@ -1085,6 +1095,7 @@ class HashtagPreferencesService { const getSchemeFamily = (s) => s === "nip44" || s === "nip44_v2" ? "nip44" : s; const seenFamilies = new Set(); + const attemptSchemes = []; const attempts = order .map((scheme) => { const family = getSchemeFamily(scheme); @@ -1092,6 +1103,7 @@ class HashtagPreferencesService { seenFamilies.add(family); const decryptFn = decryptors.get(scheme); if (!decryptFn) return null; + attemptSchemes.push(scheme); return decryptFn(ciphertext).then((plaintext) => { if (typeof plaintext !== "string") { throw new Error("Decryption returned non-string payload."); @@ -1111,7 +1123,7 @@ class HashtagPreferencesService { return { ok: true, plaintext: result.plaintext, scheme: result.scheme }; } catch (aggregateError) { const attemptErrors = (aggregateError.errors || []).map((err, i) => ({ - scheme: order[i] || "unknown", + scheme: attemptSchemes[i] || "unknown", error: err, })); const error = new Error("Failed to decrypt hashtag preferences."); diff --git a/js/subscriptions.js b/js/subscriptions.js index 42d36d81..9284b5e6 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -1116,6 +1116,7 @@ class SubscriptionsManager { const getSchemeFamily = (s) => s === "nip44" || s === "nip44_v2" ? "nip44" : s; const seenFamilies = new Set(); + const attemptSchemes = []; const attempts = order .map((scheme) => { const family = getSchemeFamily(scheme); @@ -1123,6 +1124,7 @@ class SubscriptionsManager { seenFamilies.add(family); const decryptFn = decryptors.get(scheme); if (!decryptFn) return null; + attemptSchemes.push(scheme); return decryptFn(ciphertext).then((plaintext) => { if (typeof plaintext !== "string") { throw new Error("Decryption returned a non-string payload."); @@ -1142,7 +1144,7 @@ class SubscriptionsManager { return { ok: true, plaintext: result.plaintext, scheme: result.scheme }; } catch (aggregateError) { const attemptErrors = (aggregateError.errors || []).map((err, i) => ({ - scheme: order[i] || "unknown", + scheme: attemptSchemes[i] || "unknown", error: err, })); const error = new Error("Failed to decrypt subscription list with available schemes.");