CI / Build & Quality Checks (pull_request) Successful in 3m18s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 13s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m21s
SecurityBanner asked pickBanner whether another strip would show without the dismissed set, so a dismissed Kuma announcement (often pinned for weeks) still counted and suppressed the verify / backup nudge on every device that had dismissed it. The dismissed set is now a shared jotai atom (still persisted to localStorage) used by both banners. Adds a Playwright regression. Fixes #307 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
51 lines
1.5 KiB
TypeScript
51 lines
1.5 KiB
TypeScript
import { atom } from 'jotai';
|
|
import { ServerStatus } from '../utils/kumaStatus';
|
|
|
|
/**
|
|
* [Gitea #124] The homeserver's status from its Kuma status page (null: none
|
|
* configured, not fetched yet, or Kuma unreachable — all mean "no banner"),
|
|
* plus whether this client's own connection is currently lost.
|
|
*/
|
|
export const serverStatusAtom = atom<{
|
|
status: ServerStatus | null;
|
|
syncLost: boolean;
|
|
/** When this client's connection dropped (ms), null while connected. */
|
|
lostAt: number | null;
|
|
}>({
|
|
status: null,
|
|
syncLost: false,
|
|
lostAt: null,
|
|
});
|
|
|
|
const DISMISS_KEY = 'lotus-status-banner-dismissed';
|
|
const DISMISS_MAX = 50;
|
|
|
|
const readDismissed = (): string[] => {
|
|
try {
|
|
const v = JSON.parse(localStorage.getItem(DISMISS_KEY) ?? '[]');
|
|
return Array.isArray(v) ? v.filter((k): k is string => typeof k === 'string') : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const baseDismissedAtom = atom<string[]>(readDismissed());
|
|
|
|
/**
|
|
* [Gitea #307] Status strips the user dismissed (by banner key), shared so
|
|
* the security nudge knows a dismissed announcement isn't on screen.
|
|
* Persisted to localStorage; session-only if storage is unavailable.
|
|
*/
|
|
export const statusDismissedAtom = atom(
|
|
(get) => get(baseDismissedAtom),
|
|
(get, set, key: string) => {
|
|
const next = [...get(baseDismissedAtom).filter((k) => k !== key), key].slice(-DISMISS_MAX);
|
|
try {
|
|
localStorage.setItem(DISMISS_KEY, JSON.stringify(next));
|
|
} catch {
|
|
// storage unavailable — dismissed for this session only
|
|
}
|
|
set(baseDismissedAtom, next);
|
|
},
|
|
);
|