fix(ui,i18n): E30 review round 3 — the stored language follows the screen, a fresh text starts collapsed, Retry waits again
This commit is contained in:
@@ -1,28 +1,32 @@
|
||||
// @vitest-environment jsdom
|
||||
// jsdom: the order language comes from the account's stored one, checked against i18n's languages.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { placeByPriority, priorityChoices } from "./channelOrder";
|
||||
|
||||
const row = (id: string, priority: number, title: string | null = id) => ({ id, priority, title });
|
||||
const ids = (rows: { id: string }[]) => rows.map((r) => r.id);
|
||||
const EN = { language: "en" };
|
||||
const HU = { language: "hu" };
|
||||
|
||||
describe("placeByPriority", () => {
|
||||
const list = [row("a", 10), row("b", 5), row("c", 5), row("d", 0), row("e", 0)];
|
||||
|
||||
it("bubbles a raised channel up to the first row it now outranks", () => {
|
||||
expect(
|
||||
ids(placeByPriority([...list.slice(0, 4), { ...list[4]!, priority: 5 }], "e", "en")),
|
||||
ids(placeByPriority([...list.slice(0, 4), { ...list[4]!, priority: 5 }], "e", EN)),
|
||||
).toEqual(["a", "b", "c", "e", "d"]);
|
||||
});
|
||||
|
||||
it("sinks a lowered channel below every row it no longer outranks", () => {
|
||||
expect(
|
||||
ids(placeByPriority([{ ...list[0]!, priority: 0 }, ...list.slice(1)], "a", "en")),
|
||||
).toEqual(["b", "c", "a", "d", "e"]);
|
||||
expect(ids(placeByPriority([{ ...list[0]!, priority: 0 }, ...list.slice(1)], "a", EN))).toEqual(
|
||||
["b", "c", "a", "d", "e"],
|
||||
);
|
||||
});
|
||||
|
||||
it("orders equal priorities by title, case-folded, a missing title last", () => {
|
||||
const rows = [row("x", 3, "beta"), row("y", 3, null), row("z", 3, "Alpha")];
|
||||
expect(ids(placeByPriority(rows, "z", "en"))).toEqual(["z", "x", "y"]);
|
||||
expect(ids(placeByPriority([row("y", 3, null), row("x", 3, "beta")], "y", "en"))).toEqual([
|
||||
expect(ids(placeByPriority(rows, "z", EN))).toEqual(["z", "x", "y"]);
|
||||
expect(ids(placeByPriority([row("y", 3, null), row("x", 3, "beta")], "y", EN))).toEqual([
|
||||
"x",
|
||||
"y",
|
||||
]);
|
||||
@@ -31,21 +35,31 @@ describe("placeByPriority", () => {
|
||||
it("leaves every other row where it was, even ones out of the server's order", () => {
|
||||
const rows = [row("p", 0), row("q", 9), row("r", 2)];
|
||||
// r lands before the first row it outranks; p and q keep their (unsorted) order.
|
||||
expect(ids(placeByPriority(rows, "r", "en"))).toEqual(["r", "p", "q"]);
|
||||
expect(ids(placeByPriority(rows, "r", EN))).toEqual(["r", "p", "q"]);
|
||||
});
|
||||
|
||||
it("returns an unknown id's list unchanged", () => {
|
||||
expect(ids(placeByPriority(list, "nope", "en"))).toEqual(ids(list));
|
||||
expect(ids(placeByPriority(list, "nope", EN))).toEqual(ids(list));
|
||||
});
|
||||
|
||||
it("places a title in the reader's alphabet, as the server's collation does (E30 review)", () => {
|
||||
const rows = [row("e", 0, "Ecset"), row("f", 0, "Fa"), row("m", 5, "Életrajz")];
|
||||
const moved = [...rows.slice(0, 2), { ...rows[2]!, priority: 0 }];
|
||||
expect(ids(placeByPriority(moved, "m", "hu"))).toEqual(["e", "m", "f"]);
|
||||
expect(ids(placeByPriority(moved, "m", HU))).toEqual(["e", "m", "f"]);
|
||||
const cs = [row("u", 0, "Cukor"), row("x", 0, "Dob"), row("c", 5, "Csiga")];
|
||||
expect(
|
||||
ids(placeByPriority([...cs.slice(0, 2), { ...cs[2]!, priority: 0 }], "c", "hu")),
|
||||
).toEqual(["u", "c", "x"]);
|
||||
expect(ids(placeByPriority([...cs.slice(0, 2), { ...cs[2]!, priority: 0 }], "c", HU))).toEqual([
|
||||
"u",
|
||||
"c",
|
||||
"x",
|
||||
]);
|
||||
});
|
||||
|
||||
it("places by the STORED language, whatever the screen shows: the server sorts by that", () => {
|
||||
const cs = [row("u", 0, "Cukor"), row("x", 0, "Dob"), row("c", 5, "Csiga")];
|
||||
const moved = [...cs.slice(0, 2), { ...cs[2]!, priority: 0 }];
|
||||
// None stored (a Hungarian screen picked while signed out): the server's English order.
|
||||
expect(ids(placeByPriority(moved, "c", {}))).toEqual(["c", "u", "x"]);
|
||||
expect(ids(placeByPriority(moved, "c", { language: "de" }))).toEqual(["c", "u", "x"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { compareText } from "./collate";
|
||||
import { serverOrderLang } from "./saveLanguage";
|
||||
|
||||
// The channel manager's list order, and the priority steps its menu offers (E28 S3e3b).
|
||||
|
||||
@@ -32,14 +33,16 @@ function before(a: Ordered, b: Ordered, lang: string): boolean {
|
||||
|
||||
/** `rows` with the channel `id` taken out and put back where its priority now places it, every
|
||||
* other row left exactly where it was — the server's order, without a refetch that would reorder
|
||||
* anything the reader did not touch. */
|
||||
* anything the reader did not touch. It takes the account's `preferences`, not a language: the
|
||||
* server sorts in the STORED language (`serverOrderLang`), which a UI language is not always. */
|
||||
export function placeByPriority<T extends Ordered>(
|
||||
rows: readonly T[],
|
||||
id: string,
|
||||
lang: string,
|
||||
preferences: Record<string, unknown> | null | undefined,
|
||||
): T[] {
|
||||
const moved = rows.find((r) => r.id === id);
|
||||
if (!moved) return [...rows];
|
||||
const lang = serverOrderLang(preferences);
|
||||
const rest = rows.filter((r) => r !== moved);
|
||||
const at = rest.findIndex((r) => before(moved, r, lang));
|
||||
rest.splice(at < 0 ? rest.length : at, 0, moved);
|
||||
|
||||
@@ -2,18 +2,22 @@ import type { QueryClient } from "@tanstack/react-query";
|
||||
import { isSupported } from "../i18n";
|
||||
import { api } from "./api";
|
||||
import { LANGUAGE_ORDERED } from "./queryKeys";
|
||||
import { updateMeCache } from "./useMe";
|
||||
|
||||
/** Store the reader's language choice on the server, then ask again for the lists the server orders
|
||||
* by it (`LANGUAGE_ORDERED`): only once the save has landed does the server sort in the new alphabet.
|
||||
* Rejects with the save's error; the lists are left alone then. */
|
||||
* The cached `me` takes it too, so what reads the stored language (`serverOrderLang`) agrees with the
|
||||
* server at once, not at the next `me` poll. Rejects with the save's error; nothing is touched then. */
|
||||
export async function saveLanguage(qc: QueryClient, code: string): Promise<void> {
|
||||
await api.savePrefs({ language: code });
|
||||
updateMeCache(qc, (me) => ({ ...me, preferences: { ...me.preferences, language: code } }));
|
||||
for (const queryKey of LANGUAGE_ORDERED) void qc.invalidateQueries({ queryKey });
|
||||
}
|
||||
|
||||
/** The language the server orders this account's lists in (backend `mailtext.lang_for_user`): the
|
||||
* stored preference, else English. Not always the UI's — a browser-detected language, or one picked
|
||||
* while signed out, is never stored — so a list re-slotted here to match the server's order uses it. */
|
||||
* stored preference, else English. Not always the UI's: an account with none stored stores the one
|
||||
* it is shown in at boot (BootGate), but until that save lands, or if it fails, the two differ — so a
|
||||
* list re-slotted here to match the server's order uses this, not the UI's. */
|
||||
export function serverOrderLang(preferences: Record<string, unknown> | null | undefined): string {
|
||||
const lang = preferences?.language;
|
||||
return typeof lang === "string" && isSupported(lang) ? lang : "en";
|
||||
|
||||
Reference in New Issue
Block a user