Merge main: the list subject stays notranslate and gains its translated fallback
Both sides of the conflict belong. The span holds a subject, which is the sender's words and not ours to machine-translate; the text shown when there is no subject is ours, and should follow the interface language.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_SETTINGS, DEVICE_KEYS, acceptRemote, syncedPart, type Settings } from "@/store/settings";
|
||||
import { DEFAULT_SETTINGS, DEVICE_KEYS, acceptRemote, mergeRemote, syncedPart, type Settings } from "@/store/settings";
|
||||
import { isAppFolder } from "../appFolder";
|
||||
import { settingsAlreadyLoadedFor, stopSettingsSync } from "../settingsSync";
|
||||
|
||||
/**
|
||||
* Settings used to live only in localStorage, so nothing followed the user
|
||||
@@ -81,3 +82,47 @@ describe("the client's own folder", () => {
|
||||
expect(isAppFolder({ name: "ihasmail", parentId: null, nodeType: "file" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Picking a language used to come undone.
|
||||
*
|
||||
* The subtree that reads the account's settings file is keyed on the language
|
||||
* version, so choosing a language throws it away and builds it again. The
|
||||
* remount re-read the file — which still held the old language, because the
|
||||
* write is debounced by three seconds — and applied it, putting the old
|
||||
* language back. Reported as "sometimes it takes several clicks": the click
|
||||
* that appeared to work was the one made after the previous write had landed.
|
||||
*/
|
||||
describe("a change made but not yet written up", () => {
|
||||
it("is not read back over by the file it has not reached yet", () => {
|
||||
const current: Settings = { ...DEFAULT_SETTINGS, uiLanguage: "ja" };
|
||||
const file = { uiLanguage: "en", theme: "dark" };
|
||||
const merged = mergeRemote(current, file, new Set(["uiLanguage"]));
|
||||
expect(merged.uiLanguage).toBe("ja");
|
||||
// Only the queued key is held back; the rest of the file still applies.
|
||||
expect(merged.theme).toBe("dark");
|
||||
});
|
||||
|
||||
it("applies the whole file when nothing is queued", () => {
|
||||
const current: Settings = { ...DEFAULT_SETTINGS, uiLanguage: "ja" };
|
||||
const merged = mergeRemote(current, { uiLanguage: "en" });
|
||||
expect(merged.uiLanguage).toBe("en");
|
||||
});
|
||||
|
||||
it("reads the file again for an account after a sign-out", () => {
|
||||
stopSettingsSync();
|
||||
expect(settingsAlreadyLoadedFor("a1")).toBe(false);
|
||||
// The remount that a language change causes must not read it a second time.
|
||||
expect(settingsAlreadyLoadedFor("a1")).toBe(true);
|
||||
// Signing out drops the claim, so signing back in reads the file rather
|
||||
// than trusting whatever the previous session left behind.
|
||||
stopSettingsSync();
|
||||
expect(settingsAlreadyLoadedFor("a1")).toBe(false);
|
||||
stopSettingsSync();
|
||||
});
|
||||
|
||||
it("treats a missing account as already loaded, so nothing is fetched", () => {
|
||||
expect(settingsAlreadyLoadedFor(null)).toBe(true);
|
||||
expect(settingsAlreadyLoadedFor(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ let pending: Record<string, unknown> | null = null;
|
||||
let inFlight: Promise<void> | null = null;
|
||||
/** Nothing is pushed before the first load has settled, or we would race it. */
|
||||
let armed = false;
|
||||
let loadedFor: string | null = null;
|
||||
let listenersBound = false;
|
||||
|
||||
export function settingsSyncAvailable(): boolean {
|
||||
@@ -62,6 +63,24 @@ export async function loadRemoteSettings(): Promise<Record<string, unknown> | nu
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Has this account's settings file already been read on this page load?
|
||||
*
|
||||
* Claims the account as a side effect, so two callers cannot both start a
|
||||
* read. The subtree that does the reading is keyed on the language version
|
||||
* and so is deliberately remounted whenever somebody picks a language;
|
||||
* without this the remount re-reads a file written before the change and
|
||||
* applies it, putting the old language back.
|
||||
*
|
||||
* Cleared by `stopSettingsSync`, so signing out and back in reads again.
|
||||
*/
|
||||
export function settingsAlreadyLoadedFor(accountId: string | null | undefined): boolean {
|
||||
if (!accountId) return true;
|
||||
if (loadedFor === accountId) return true;
|
||||
loadedFor = accountId;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Allow pushes. Called once the first load has settled, either way. */
|
||||
export function armSettingsSync(): void {
|
||||
armed = true;
|
||||
@@ -71,6 +90,7 @@ export function armSettingsSync(): void {
|
||||
/** Stop syncing and drop anything queued (logout). */
|
||||
export function stopSettingsSync(): void {
|
||||
armed = false;
|
||||
loadedFor = null;
|
||||
pending = null;
|
||||
if (timer !== null) {
|
||||
window.clearTimeout(timer);
|
||||
@@ -93,6 +113,20 @@ export function queueSettingsPush(synced: Record<string, unknown>): void {
|
||||
}, DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys of a change that has been made but not yet written up.
|
||||
*
|
||||
* `hydrate` needs these: a settings file read from the server is older than an
|
||||
* unflushed local change by definition, so applying it wholesale hands the
|
||||
* user back the value they just replaced. Switching language made that visible
|
||||
* — it remounts the tree, the remount re-reads the file, and the file still
|
||||
* says the old language — but the race is general and a slow read would lose
|
||||
* any click made inside the debounce window.
|
||||
*/
|
||||
export function pendingSettingsKeys(): ReadonlySet<string> {
|
||||
return new Set(pending ? Object.keys(pending) : []);
|
||||
}
|
||||
|
||||
/** Write anything queued now, rather than waiting out the debounce. */
|
||||
export async function flushSettingsPush(): Promise<void> {
|
||||
if (timer !== null) {
|
||||
|
||||
Reference in New Issue
Block a user