fix: handle permission-required errors in hashtag preferences background loading

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
This commit is contained in:
Claude committed 2026-02-07 05:42:24 +00:00
1 parent 0c85e0ea7f
commit 1403d2d4cc
2 files changed
+16 -2

No files matched your search

+13 -1
View File
@@ -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.");
+3 -1
View File
@@ -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.");