fix(seasonal): auto theme re-evaluates over time; auto clears chat background
- The "auto" seasonal theme was computed once at mount, so a long-lived session
never crossed a season/holiday-window boundary. SeasonalEffect now re-evaluates
on an hourly ticker (auto mode only) AND refreshes on entering auto — the
interval only runs while auto, so a stale mount-time timestamp would otherwise
resurface on a pinned/off → auto switch (the exact frozen-at-mount bug, caught
in review). The decision is extracted to a pure resolveSeasonTheme(override,
now) in seasonSchedule.ts (removing an unsafe cast) and unit-tested.
- Selecting seasonal "auto" while a chat background was set was a silent no-op:
the seasonal picker only cleared the background for a *specific* theme, and the
overlay is suppressed while a background is set. Now any active seasonal mode
("auto" included) clears the background; only "off" leaves it — symmetric with
the background picker (which sets seasonal "off"). The overlay guard stays as a
backstop for legacy persisted state.
Bug-hunt findings from LOTUS_TODO. Three review passes (the 2nd caught the
switch-into-auto staleness); +2 unit tests. Gate-green (tsc, eslint, prettier,
922 tests, build).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useReducedMotion } from '../../hooks/useReducedMotion';
|
||||
import { zIndices } from '../../styles/zIndex';
|
||||
import { SeasonTheme } from './types';
|
||||
import { getActiveSeason } from './seasonSchedule';
|
||||
import { resolveSeasonTheme } from './seasonSchedule';
|
||||
import { HalloweenOverlay } from './themes/Halloween';
|
||||
import { ChristmasOverlay } from './themes/Christmas';
|
||||
import { NewYearOverlay } from './themes/NewYear';
|
||||
@@ -96,13 +96,25 @@ export function SeasonalPreview({ theme }: { theme: SeasonTheme }) {
|
||||
export function SeasonalEffect() {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const reduced = useReducedMotion();
|
||||
const override = settings.seasonalThemeOverride ?? 'auto';
|
||||
|
||||
const theme = useMemo<SeasonTheme | null>(() => {
|
||||
const override = settings.seasonalThemeOverride ?? 'auto';
|
||||
if (override === 'off') return null;
|
||||
if (override === 'auto') return getActiveSeason(new Date());
|
||||
return override as SeasonTheme;
|
||||
}, [settings.seasonalThemeOverride]);
|
||||
// In auto mode, re-evaluate hourly so a long-lived session crosses a
|
||||
// season/holiday-window boundary (e.g. into a new day) without a reload —
|
||||
// otherwise the active season is frozen at the value it had on mount.
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
useEffect(() => {
|
||||
if (override !== 'auto') return undefined;
|
||||
// Refresh on entering auto too: `now` may be a stale mount-time value if we
|
||||
// were previously in a pinned/off mode (the interval only runs while auto).
|
||||
setNow(Date.now());
|
||||
const id = window.setInterval(() => setNow(Date.now()), 60 * 60 * 1000);
|
||||
return () => window.clearInterval(id);
|
||||
}, [override]);
|
||||
|
||||
const theme = useMemo<SeasonTheme | null>(
|
||||
() => resolveSeasonTheme(override, now),
|
||||
[override, now],
|
||||
);
|
||||
|
||||
if (!theme) return null;
|
||||
// Suppress seasonal overlay when a chat background is active — both running simultaneously
|
||||
|
||||
Reference in New Issue
Block a user