diff --git a/README.md b/README.md index d16c912..fdd6219 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,12 @@ Alongside them sits **today's dailies** — commissions, sanity, daily training No wiki publishes those, so they are a fixed list in the app rather than scraped data, and they are the only thing on the page that expires tonight rather than next patch. +Because nobody publishes them, they are the app's guess at your routine rather than anything you +asked for — so **Show each game's own daily chores** in the settings turns them off. Only the +invented chores go: anything with a checklist is something you engaged with, including events you +added yourself and marked daily, and those stay where they are. Nothing is discarded, so every tick +and streak is waiting if you turn it back on. + Both roll over at **04:00 server time** in your region, not midnight, because that is when the games roll over. Finishing at 02:00 still counts as yesterday. diff --git a/src/client/App.tsx b/src/client/App.tsx index 354476a..21bba37 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -538,6 +538,7 @@ export function App() { the lanes out here would also cost a reader's own game its place in the order their repeating events are grouped under. */} r.event)} region={prefs.region} diff --git a/src/client/components/Controls.tsx b/src/client/components/Controls.tsx index b670e94..1bdb438 100644 --- a/src/client/components/Controls.tsx +++ b/src/client/components/Controls.tsx @@ -230,6 +230,18 @@ export function Controls({ hint="Guessed from what the source wrote, so it misses some and invents others. Off, only events you mark yourself get a checklist. Your ticks and streaks are kept either way." /> + {/* The chores are the app's own invention — no source publishes + "Commissions, resin" — and they were the one part of the strip + with no way out. Named for what it removes rather than for + "dailies", which would read as broken while the strip stayed on + screen showing the reader's own events. */} + onUpdate({ showChores })} + label="Show each game's own daily chores" + hint="Commissions, resin, daily training and the rest. Nobody publishes these, so the app keeps a fixed list per game. Off, the strip shows only events with a checklist. Your ticks and streaks are kept either way." + /> + {ignoredCount > 0 && ( GameMeta, startOf: (event: DisplayEvent) => number, + showChores = true, ): DailyGroup[] { // A lane can arrive through an event without being in `games` — an event on a // game the reader has since switched off, say — and dropping it here would @@ -102,7 +103,13 @@ export function dailyGroups( const today = dayKey(now, region, lane); const resetsIn = msUntilReset(now, region, lane); - if (!isCustomGameId(lane)) { + // The standing chore is the app's own invention — no source publishes + // "Commissions, resin" — so it is the one part of this strip a reader can + // reasonably want gone. Switched off it is simply not built; nothing reads + // or writes its ticks here, so `dailies:` keeps every day the reader + // ever logged and switching back on restores the lot. A game the reader + // invented never had one to begin with. + if (showChores && !isCustomGameId(lane)) { items.push({ key: dailiesId(lane as GameId), game: lane, @@ -141,6 +148,7 @@ export function Dailies({ now, daysFor, onToggleDay, + showChores, }: { /** Every lane the reader is looking at, in their own order. */ games: LaneId[]; @@ -151,6 +159,8 @@ export function Dailies({ */ events: DisplayEvent[]; region: Region; + /** Whether to carry each game's standing chore — `prefs.showChores`. */ + showChores: boolean; now: number; daysFor: (id: string) => string[]; onToggleDay: (id: string, day: string) => void; @@ -176,6 +186,7 @@ export function Dailies({ region, gameMeta, (event) => Date.parse(event.startsAt), + showChores, ); const items = groups.flatMap((group) => group.items); const total = items.length; diff --git a/src/client/state/usePrefs.ts b/src/client/state/usePrefs.ts index 5bcd63b..ae9eb84 100644 --- a/src/client/state/usePrefs.ts +++ b/src/client/state/usePrefs.ts @@ -155,6 +155,18 @@ export interface Prefs { * who already switched it on keep it — stored prefs win over this default. */ detectDaily: boolean; + /** + * Whether Today's dailies carries each game's standing chore — commissions, + * sanity, daily training. + * + * On by default, because every reader has these today and a setting that + * silently removes something on upgrade is worse than one nobody notices. + * Off hides only the chores: an event with a checklist is something the + * reader engaged with and stays either way. Like `detectDaily` it discards + * nothing — the ticks live under `dailies:` and are never written from + * here, so switching back on restores every one of them. + */ + showChores: boolean; showCompleted: boolean; /** Reveal events the reader has ignored, so they can be restored. */ showIgnored: boolean; @@ -186,6 +198,7 @@ function defaults(): Prefs { showUpcoming: false, timelineSplitUpcoming: true, detectDaily: false, + showChores: true, showCompleted: true, showIgnored: false, theme: DEFAULT_THEME_CHOICE, diff --git a/test/controls.test.tsx b/test/controls.test.tsx index f520828..89afa2a 100644 --- a/test/controls.test.tsx +++ b/test/controls.test.tsx @@ -30,6 +30,7 @@ const PREFS: Prefs = { showUpcoming: false, timelineSplitUpcoming: true, detectDaily: false, + showChores: true, showCompleted: true, showIgnored: false, theme: "dark", @@ -99,11 +100,13 @@ describe("Controls: what am I allowed to look at", () => { test("it reads its own preference and not a neighbour's", () => { // Both neighbours are on and this one is off, so a checkbox bound to the - // wrong key shows up as the wrong count of ticks. + // wrong key shows up as the wrong count of ticks. The chores toggle is a + // third box that is on in both renders — it defaults on — so it raises + // each count by one without changing what the delta proves. const off = checkboxes(render(PREFS)); const on = checkboxes(render({ ...PREFS, showUpcoming: true })); - expect(off.filter(Boolean)).toHaveLength(1); - expect(on.filter(Boolean)).toHaveLength(2); + expect(off.filter(Boolean)).toHaveLength(2); + expect(on.filter(Boolean)).toHaveLength(3); }); test("how unstarted events sit on the board is offered only when they are", () => { diff --git a/test/views.test.tsx b/test/views.test.tsx index f4ca1f3..3b64798 100644 --- a/test/views.test.tsx +++ b/test/views.test.tsx @@ -1014,3 +1014,77 @@ describe("boardWindow is not widened by expansion", () => { expect(chartWidthOf(withExpand)).toBe(chartWidthOf(withoutExpand)); }); }); + +describe("dailyGroups with the chores switched off", () => { + const NOW = Date.parse("2026-08-17T12:00:00.000Z"); + const meta = (id: string) => metaFor(id, {}); + const startOf = (e: { startsAt: string }) => Date.parse(e.startsAt); + const repeating = (id: string, game: string, title: string) => + ({ + id, + game, + title, + type: "login", + summary: null, + startsAt: "2026-08-10T00:00:00.000Z", + startPrecision: "day", + endsAt: null, + endPrecision: "unknown", + regionScoped: false, + regionEnds: null, + sourceUrl: "https://example.test", + }) as never; + + test("the invented chore goes and the events stay", () => { + // Only the fixed per-game list the app makes up goes. Anything with a + // checklist is something the reader engaged with, and stays. + const groups = dailyGroups( + ["genshin"], + [repeating("e1", "genshin", "Login Bonus")], + NOW, + "europe", + meta, + startOf, + false, + ); + expect(groups.map((g) => g.items.map((i) => i.key))).toEqual([["e1"]]); + }); + + test("an event the reader added themselves is untouched", () => { + // Stated explicitly because it is the requirement most at risk of being + // filtered away by a switch aimed at something else. + const groups = dailyGroups( + ["genshin"], + [repeating("myevent:k3f9qa2m01", "genshin", "My own grind")], + NOW, + "europe", + meta, + startOf, + false, + ); + expect(groups[0]?.items.map((i) => i.key)).toEqual(["myevent:k3f9qa2m01"]); + }); + + test("with nothing else to show, a game contributes no group at all", () => { + // Not an empty group with a heading and no rows — the strip's own + // `total === 0` guard then drops it entirely, which is what a reader who + // turned this off is asking for. + const groups = dailyGroups(["genshin", "hsr"], [], NOW, "europe", meta, startOf, false); + expect(groups).toEqual([]); + }); + + test("left on, nothing about the strip moves", () => { + const groups = dailyGroups( + ["genshin"], + [repeating("e1", "genshin", "Login Bonus")], + NOW, + "europe", + meta, + startOf, + true, + ); + expect(groups.map((g) => g.items.map((i) => i.key))).toEqual([ + ["dailies:genshin", "e1"], + ]); + }); +});