diff --git a/src/client/App.tsx b/src/client/App.tsx index e86b361..e93cbe4 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -19,7 +19,7 @@ import { useProgress } from "./state/useProgress.ts"; import { useDailyLog, type DailyLogMap } from "./state/useDailyLog.ts"; import { useTasks } from "./state/useTasks.ts"; import { useAmounts } from "./state/useAmounts.ts"; -import { adoptNewLanes, usePrefs, type Prefs } from "./state/usePrefs.ts"; +import { adoptNewLanes, firstRunPrefs, usePrefs, type Prefs } from "./state/usePrefs.ts"; import { buildExportData, parseImportData, triggerDownload } from "./state/export.ts"; import { snapDayWidth } from "./state/zoom.ts"; import { useCustom, type EventDraft } from "./state/useCustom.ts"; @@ -487,11 +487,7 @@ export function App() { - update({ - onboarded: true, - hiddenGames: games.filter((g) => !chosen.includes(g)), - view: chosenView, - }) + update(firstRunPrefs(games, chosen, chosenView)) } /> diff --git a/src/client/state/usePrefs.ts b/src/client/state/usePrefs.ts index 44a9555..d03bdb0 100644 --- a/src/client/state/usePrefs.ts +++ b/src/client/state/usePrefs.ts @@ -7,6 +7,7 @@ import { KEYS, readJson, writeJson } from "./storage.ts"; import type { TimelineGroup } from "./lanes.ts"; import { DEFAULT_THEME_CHOICE, type ThemeChoice } from "./theme.ts"; import { DEFAULT_DAY_WIDTH, snapDayWidth } from "./zoom.ts"; +import { ENTRIES, newestId } from "../../shared/changelog.ts"; /** * Which of the two views the reader is looking at. @@ -192,6 +193,30 @@ export interface Prefs { routineGames?: LaneId[] | undefined; /** Games already asked "track its weekly tasks?", so the question is asked once. */ routinesAsked?: LaneId[] | undefined; + /** + * The newest changelog entry the reader dismissed (PRD F18). Absent means + * unrecorded, not "seen nothing": first-run setup sets it, so an absent value + * is an install from before the changelog, shown the launch window once. + */ + changelogSeen?: string | undefined; +} + +/** + * What finishing first-run setup writes. A new reader is marked up to date + * with the changelog: a list of changes means nothing before you've used the + * thing it changes. + */ +export function firstRunPrefs( + games: readonly LaneId[], + chosen: readonly LaneId[], + view: View, +): Partial { + return { + onboarded: true, + hiddenGames: games.filter((g) => !chosen.includes(g)), + view, + changelogSeen: newestId(ENTRIES), + }; } export function defaults(): Prefs { @@ -321,6 +346,10 @@ export function restorePrefsValue(incoming: unknown): Prefs { } } + if (result.changelogSeen !== undefined && typeof result.changelogSeen !== "string") { + result.changelogSeen = undefined; + } + // Built-in tasks: string arrays, or absent. for (const key of ["routineGames", "routinesAsked"] as const) { const value: unknown = result[key]; diff --git a/test/changelog.test.ts b/test/changelog.test.ts index 7d96ed2..85911a2 100644 --- a/test/changelog.test.ts +++ b/test/changelog.test.ts @@ -63,3 +63,22 @@ describe("what a reader has not seen", () => { expect(compareUrl("https://r", "a1b2c3d", "e4f5a6b")).toBe("https://r/compare/a1b2c3d...e4f5a6b"); }); }); + +describe("the seen marker in prefs", () => { + test("restore keeps a string and drops anything else", async () => { + const { restorePrefsValue } = await import("../src/client/state/usePrefs.ts"); + expect(restorePrefsValue({ changelogSeen: "check-in-goals" }).changelogSeen).toBe("check-in-goals"); + expect(restorePrefsValue({ changelogSeen: 7 }).changelogSeen).toBeUndefined(); + expect(restorePrefsValue({}).changelogSeen).toBeUndefined(); + }); + + test("first-run setup marks everything seen", async () => { + const { firstRunPrefs } = await import("../src/client/state/usePrefs.ts"); + expect(firstRunPrefs(["genshin", "hsr"], ["hsr"], "soon")).toEqual({ + onboarded: true, + hiddenGames: ["genshin"], + view: "soon", + changelogSeen: newestId(ENTRIES), + }); + }); +});