Let each account carry its own theme - #303
Merged
Merged
Conversation
With several accounts the panel looked identical whether you were in your main identity or a throwaway, and the account name was the only thing telling them apart. A theme answers "which account am I in" before you read anything. Same map-with-fallback shape as nip65OnlyBy and defaultClientBy, including its own message: SIDECAR_SET_SETTINGS merges shallowly, so a panel sending the whole map would clobber another account's theme and racing panels would lose one. Clearing deletes the key rather than storing an empty string. THREE SURFACES READ THE THEME AND THEY DO NOT ALL FOLLOW THE ACCOUNT. That asymmetry is the design, not an oversight: - The panel follows it. The obvious one. - The approval window follows it, and is the valuable one. That window is about a specific identity, and it is where picking the wrong one costs money — so it is the surface a per-account theme most needs to reach, not an afterthought. - The pay card DOES NOT. content.js reads settings through a deliberately clamped path so a visited site cannot fingerprint config. Handing it a per-account theme would tell the site WHICH ACCOUNT IS ACTIVE ON IT. content.js is untouched and there is a test that fails if themeBy ever appears in it. Two edges the issue asked about. Locked falls back to the global, because the lock screen paints before the panel knows who is unlocking. And the theme changes on the switch itself: every account switch already calls refresh(), which re-runs the boot apply, so nothing extra was needed. The card grid has no "off" position, so a line under the heading says which state the account is in and carries the way back to the global. Without it an account could take a theme and never give it back. Onboarding still sets the global, since before any account exists there is nobody to attribute the choice to and the global is what every new account inherits. trust-nudge.test.js pinned the exact old payload expression; updated to assert the account resolution and the global fallback separately.
"Following the default for all accounts." was explaining the mechanism when the only thing the line needs to say is which theme you are looking at. "Using default theme" says it in three words.
Regression from the previous commit. Once the picker wrote only themeBy, NOTHING wrote settings.theme any more except the onboarding branch — so the moment a user had an account the global froze, and every surface reading it froze with it. The pay card in content.js reads the global deliberately (it must not track the active account, or it tells a page which account that is), so it stayed on whatever theme was set before the first account existed, forever. The picker now writes both. The global is demoted to what it should have been from the start: the default a new account inherits, and what the surfaces that cannot follow the account read. This does move accounts that have never chosen one, which is the same rule the client picker already follows — a default is a default, and changing it moves everyone still following it. It does not reintroduce the leak #266 was protecting against. The card changes when a theme is PICKED, never when accounts are SWITCHED, so a page still cannot learn which account is active on it.
Reverts the previous commit. Writing settings.theme on every pick meant one account's choice became the default, so a second account that had never chosen showed the first account's theme instead of the built-in default. That is the opposite of what per-account themes are for, and it is worse than the problem it was solving. The cost is back: settings.theme is only writable during onboarding, so the pay card shows the default rather than tracking picks. content.js reads the global deliberately — it must not follow the active account or it tells a page which account that is — so a stable card is the lesser problem here. Making the default settable again needs its own design, not a side effect of the picker. Noted on #266.
Two reported bugs, one cause: settings.theme was doing two jobs at once, the pay card's palette AND the default an account that never chose inherits, so every arrangement of that one field failed differently. A pick that wrote only themeBy froze the card on whatever was set before the first account existed. Writing both handed one account's pick to every account that had never chosen. Letting the account's theme win over the gallery made all twelve cards inert for any account that had one, and a separate cardTheme written per pick matched only the account picked for most recently. So the gallery goes back to meaning what #266 asked for: it dresses the account you are in, writing themeBy and leaving settings.theme alone as the default a new account inherits. Onboarding still writes the default, since before any account exists there is nobody to attribute a choice to. The account's own menu keeps a dropdown for dressing any other account, and it is the only way back to the default now that every card is a theme. The card is the part that took five tries, because it renders into a page that can see it. It cannot follow the active account: a color change on a switch is a switch detector a page can poll by cycling its own invoice, and the active identity may be one the site has never seen. It follows the account THAT SITE IS BOUND TO instead, whose pubkey the site already holds, so the palette tells it nothing it does not know. background.js resolves it in the clamped GET_SETTINGS, where the binding and the settings can be joined, and themeBy still never crosses into a content script. It is also less of a fingerprint than the single global it replaces, which showed two sites bound to two accounts one shared value. Marking the gallery moved out of applyTheme into paintThemePicker, since applyTheme runs on every render and was mounting twelve preview documents on the lock screen to mark a card nobody was looking at. Settings mounts them when it opens. The account modal also lost a race where the value arriving from the background could overwrite a pick made in that instant, and its buttons now sit in the shared full-width actions row instead of loose in the sheet.
dmnyc
added a commit
that referenced
this pull request
Sep 9, 2026
A theme belongs to an account, not to the install (themeBy, #266/#303), so reading only settings.theme would have handed these pages the default that an account which never chose inherits. That is this branch's own bug back again for anyone using per-account themes: on Par Avion, opening the guide would still give you a dark page out of nowhere. theme-boot now resolves sidecar_active_pubkey against themeBy and falls back to settings.theme. Both keys are a plain chrome.storage.local read, so nothing has to be asked of the background. RESOLVED HERE, unlike the pay card, and the difference is worth stating because they look like the same problem. That card renders into a web page that can see it, so it must never learn which account is active and takes the theme of the account its own site is bound to instead. help.html, welcome.html and wallets.html are chrome-extension:// documents that nothing on the web can read, so the account on screen is simply the right answer. The change is a no-op until #303 lands: with no themeBy in storage the first term is undefined and it falls through to settings.theme, which is exactly what shipped before. So this does not care which of the two merges first, and the two branches share no files, meaning they would otherwise have merged clean and been silently wrong. The storage listener now watches sidecar_active_pubkey as well, and re-reads rather than pulling a value out of the change record, because the answer needs both keys and each one moves alone. Switching account now repaints a guide already open in another tab, the way a theme change already did. Verified in a browser across all four states: no themeBy at all wears the global and keeps .theme-light; giving the active account Nixie repainted an already-open help.html live and dropped the class; switching to an account on Bauhaus followed and restored it; clearing that account's theme fell back to the default. Suite: 1,034 pass, with search-entity and web-comment still red on the pre-existing missing nostr-tools that #296 fixes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #266. Stacked on #302, which established the storage shape.
With several accounts the panel looked identical whether you were in your main identity or a throwaway, and the account name was the only thing telling them apart. A theme answers "which account am I in" before you read anything.
The asymmetry is the design
Three surfaces read the theme and they do not all follow the account:
content.jsreads settings through a deliberately clamped path so a visited site can't fingerprint config. Handing it a per-account theme would tell the site which account is active on it. The file is untouched, and there's a test that fails ifthemeByever appears in it.Storage
Same map-with-fallback as
nip65OnlyByanddefaultClientBy, including its own message for the same documented reason:SIDECAR_SET_SETTINGSmerges shallowly, so a panel sending the whole map would clobber another account's theme and racing panels would lose one. Clearing deletes the key rather than storing an empty string.The two edges #266 asked about
Locked falls back to the global. The lock screen paints before the panel knows who's unlocking.
When the switch applies: on the switch itself. Every account switch already calls
refresh(), which re-runs the boot apply, so nothing extra was needed. No preview, per the issue's own instinct that it'd be a distraction.The way back
The card grid has no "off" position, so an account could take a theme and never give it back. A line under the heading says which state you're in and carries the exit:
Onboarding still writes the global, since before any account exists there's nobody to attribute the choice to.
Verification
12 unit tests, plus exercised in a real panel end to end: picking Nixie wrote
themeBy[pubkey]and left the global unset, and "Use the default" cleared the key and reverted the panel to Speakeasy.trust-nudge.test.jspinned the exact old payload expression, so it's updated to assert the account resolution and the global fallback separately rather than weakened.