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:
@@ -85,6 +85,8 @@ export function settingsAlreadyLoadedFor(accountId: string | null | undefined):
|
||||
export function armSettingsSync(): void {
|
||||
armed = true;
|
||||
bindFlushListeners();
|
||||
// A change made while the read was in flight has been waiting for this.
|
||||
if (pending) void flushSettingsPush();
|
||||
}
|
||||
|
||||
/** Stop syncing and drop anything queued (logout). */
|
||||
@@ -104,8 +106,22 @@ export function stopSettingsSync(): void {
|
||||
* one request goes out once the changes stop.
|
||||
*/
|
||||
export function queueSettingsPush(synced: Record<string, unknown>): void {
|
||||
if (!armed || !settingsSyncAvailable()) return;
|
||||
if (!settingsSyncAvailable()) return;
|
||||
/*
|
||||
* Held, not dropped, before the first load has settled.
|
||||
*
|
||||
* This used to return here, which silently threw the change away: a
|
||||
* language picked in the second or so before the settings file came back
|
||||
* was never written, so it survived until the next reload and no further.
|
||||
* That is the other half of "sometimes it takes several clicks" -- the
|
||||
* click that stuck was one made after the read had finished.
|
||||
*
|
||||
* Keeping it is safe because `hydrate` refuses to overwrite a key that is
|
||||
* still queued, so the newer local change wins over the older file rather
|
||||
* than racing it. `armSettingsSync` writes whatever is waiting.
|
||||
*/
|
||||
pending = synced;
|
||||
if (!armed) return;
|
||||
if (timer !== null) window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => {
|
||||
timer = null;
|
||||
|
||||
@@ -107,6 +107,25 @@ export function loadJson<T>(key: string, fallback: T): T {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Did `loadJson` have something to return, or did it hand back the fallback?
|
||||
*
|
||||
* The difference decides whether a first paint is worth anything. With a
|
||||
* cached value the screen can be right immediately and the account's copy only
|
||||
* has to correct it; without one -- an untrusted device, or the sign-out that
|
||||
* every deploy causes -- the first paint is the defaults, and painting it
|
||||
* before the account's settings arrive shows English to somebody who chose
|
||||
* otherwise.
|
||||
*/
|
||||
export function hasCachedJson(key: string): boolean {
|
||||
if (!trusted) return false;
|
||||
try {
|
||||
return localStorage.getItem(PREFIX + key) != null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadRaw<T>(key: string, fallback: T): T {
|
||||
if (!trusted) return fallback;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user