Keep a settings change made before the first read, and wait for it

Two defects on the path that decides what language the app starts in.

**A change made before the settings file came back was thrown away.**
`queueSettingsPush` returned early while unarmed, dropping the value
instead of holding it, so a language picked in the second or so after a
page load was never written up: it survived until the next reload and no
further. That is a better account of "sometimes it takes several clicks"
than the remount race fixed in #160 — the click that stuck was one made
after the read had finished. Keeping it is safe because hydrate already
refuses to overwrite a key that is still queued.

Proof it was real: before this, no `ihasmail` folder was ever created in
the account's files, because the seed write never fired. After it, the
folder appears.

**Without a cached settings object the tree painted too early.** The
cache is not read on an untrusted device, and it is cleared by the
sign-out that every deploy causes, so in both cases the first frame is
the defaults — and the defaults are English. Anything computed in that
window is computed in the wrong language. The interface recovers, since
it is rebuilt when the catalogue lands, but a string emitted once does
not: this is why the stale-folder toast came out in English on an
otherwise German screen.

So without a cache the authenticated tree now waits for the account's
settings and their catalogue, which costs nothing — there was nothing
worth painting yet. With a cache it does not wait, and the first frame is
as quick as it was.

Neither fix makes the toast German yet: the account settings file is
neither written nor read successfully in the mock, and both failures are
swallowed. That is a third problem, and this commit does not touch it.
This commit is contained in:
2026-08-31 15:03:40 -07:00
parent 67f5a88ff6
commit 635c4c7e52
4 changed files with 92 additions and 12 deletions
+12 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { create } from "zustand";
import { loadJson, saveJson } from "@/lib/storage";
import { hasCachedJson, loadJson, saveJson } from "@/lib/storage";
import { pendingSettingsKeys, queueSettingsPush } from "@/lib/settingsSync";
import { setDateTimePrefs, setUiLanguageForFormatting, type DateFormat, type TimeFormat } from "@/lib/datetime";
import type { SwipeAction } from "@/lib/swipe";
@@ -311,6 +311,17 @@ interface SettingsState {
}
const initialSettings = loadJson<Settings>("settings", DEFAULT_SETTINGS);
/**
* Whether the first frame is this account's settings or merely the defaults.
*
* False after every deploy, because deploys sign everyone out and sign-out
* clears the cache -- and false on any untrusted device, where the cache is
* never read. In that state `uiLanguage` starts as English and only becomes
* the account's choice once the settings file lands, which is why the
* authenticated tree waits for it.
*/
export const PAINTED_FROM_CACHE = hasCachedJson("settings");
applyDateTimePrefs(initialSettings);
export const useSettings = create<SettingsState>((set, get) => ({