mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-08 21:05:21 +00:00
Pref context
This commit is contained in:
+15
-5
@@ -10,6 +10,16 @@ export const THEME = {
|
||||
// index.html reads it synchronously to pick the splash background before first paint.
|
||||
export const THEME_LOCALSTORAGE_KEY = "theme";
|
||||
|
||||
// Default values returned by the getters below when a preference hasn't been set. Single source of
|
||||
// truth, also used by the PrefCache (see PrefCache.jsx) for its loading-state placeholder.
|
||||
export const PREF_DEFAULTS = {
|
||||
sound: "ding",
|
||||
minPriority: 1,
|
||||
deleteAfter: 604800, // one week
|
||||
theme: THEME.SYSTEM,
|
||||
webPushEnabled: false,
|
||||
};
|
||||
|
||||
const mirrorThemeToLocalStorage = (value) => {
|
||||
try {
|
||||
localStorage.setItem(THEME_LOCALSTORAGE_KEY, value);
|
||||
@@ -30,7 +40,7 @@ class Prefs {
|
||||
|
||||
async sound() {
|
||||
const sound = await this.db.prefs.get("sound");
|
||||
return sound ? sound.value : "ding";
|
||||
return sound ? sound.value : PREF_DEFAULTS.sound;
|
||||
}
|
||||
|
||||
async setMinPriority(minPriority) {
|
||||
@@ -39,7 +49,7 @@ class Prefs {
|
||||
|
||||
async minPriority() {
|
||||
const minPriority = await this.db.prefs.get("minPriority");
|
||||
return minPriority ? Number(minPriority.value) : 1;
|
||||
return minPriority ? Number(minPriority.value) : PREF_DEFAULTS.minPriority;
|
||||
}
|
||||
|
||||
async setDeleteAfter(deleteAfter) {
|
||||
@@ -48,12 +58,12 @@ class Prefs {
|
||||
|
||||
async deleteAfter() {
|
||||
const deleteAfter = await this.db.prefs.get("deleteAfter");
|
||||
return deleteAfter ? Number(deleteAfter.value) : 604800; // Default is one week
|
||||
return deleteAfter ? Number(deleteAfter.value) : PREF_DEFAULTS.deleteAfter;
|
||||
}
|
||||
|
||||
async webPushEnabled() {
|
||||
const webPushEnabled = await this.db.prefs.get("webPushEnabled");
|
||||
return webPushEnabled?.value;
|
||||
return webPushEnabled?.value ?? PREF_DEFAULTS.webPushEnabled;
|
||||
}
|
||||
|
||||
async setWebPushEnabled(enabled) {
|
||||
@@ -62,7 +72,7 @@ class Prefs {
|
||||
|
||||
async theme() {
|
||||
const theme = await this.db.prefs.get("theme");
|
||||
const value = theme?.value ?? THEME.SYSTEM;
|
||||
const value = theme?.value ?? PREF_DEFAULTS.theme;
|
||||
// Mirror to localStorage so the inline script in index.html can pick the splash background
|
||||
// synchronously before first paint. Self-heals for users who set their theme before the
|
||||
// mirror existed.
|
||||
|
||||
+23
-20
@@ -28,6 +28,7 @@ import prefs from "../app/Prefs";
|
||||
import RTLCacheProvider from "./RTLCacheProvider";
|
||||
import session from "../app/Session";
|
||||
import AccountContext from "./AccountContext";
|
||||
import { PrefCacheProvider } from "./PrefCache";
|
||||
import hideSplash from "../app/splash";
|
||||
|
||||
initI18n();
|
||||
@@ -148,27 +149,29 @@ const Layout = () => {
|
||||
}, [subscriptions]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<ActionBar selected={selected} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} />
|
||||
<Navigation
|
||||
subscriptions={subscriptionsWithoutInternal}
|
||||
selectedSubscription={selected}
|
||||
mobileDrawerOpen={mobileDrawerOpen}
|
||||
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
||||
onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)}
|
||||
/>
|
||||
<Main>
|
||||
<Toolbar />
|
||||
<Outlet
|
||||
context={{
|
||||
subscriptions: subscriptionsWithoutInternal,
|
||||
selected,
|
||||
allNotifications,
|
||||
}}
|
||||
<PrefCacheProvider>
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<ActionBar selected={selected} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} />
|
||||
<Navigation
|
||||
subscriptions={subscriptionsWithoutInternal}
|
||||
selectedSubscription={selected}
|
||||
mobileDrawerOpen={mobileDrawerOpen}
|
||||
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
||||
onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)}
|
||||
/>
|
||||
</Main>
|
||||
<Messaging selected={selected} dialogOpenMode={sendDialogOpenMode} onDialogOpenModeChange={setSendDialogOpenMode} />
|
||||
</Box>
|
||||
<Main>
|
||||
<Toolbar />
|
||||
<Outlet
|
||||
context={{
|
||||
subscriptions: subscriptionsWithoutInternal,
|
||||
selected,
|
||||
allNotifications,
|
||||
}}
|
||||
/>
|
||||
</Main>
|
||||
<Messaging selected={selected} dialogOpenMode={sendDialogOpenMode} onDialogOpenModeChange={setSendDialogOpenMode} />
|
||||
</Box>
|
||||
</PrefCacheProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import prefs, { PREF_DEFAULTS } from "../app/Prefs";
|
||||
|
||||
// A render-time CACHE of the user's preferences -- NOT the source of truth. The source of truth is
|
||||
// the `prefs` table in IndexedDB, always read/written via Prefs.js. This context preloads all prefs
|
||||
// once (mounted by Layout, above the routed content), so the Settings page renders its controls
|
||||
// with the right values immediately instead of each one flickering in as its own IndexedDB read
|
||||
// resolves. By the time you navigate to Settings the query has already resolved.
|
||||
const PrefCacheContext = createContext(undefined);
|
||||
|
||||
export const PrefCacheProvider = ({ children }) => {
|
||||
const cache = useLiveQuery(async () => ({
|
||||
sound: await prefs.sound(),
|
||||
minPriority: await prefs.minPriority(),
|
||||
deleteAfter: await prefs.deleteAfter(),
|
||||
theme: await prefs.theme(),
|
||||
webPushEnabled: await prefs.webPushEnabled(),
|
||||
}));
|
||||
return <PrefCacheContext.Provider value={cache}>{children}</PrefCacheContext.Provider>;
|
||||
};
|
||||
|
||||
// While the cache is still loading (only on the very first paint, mostly hidden by the splash) fall
|
||||
// back to the same defaults the Prefs getters use, so controls never render with no value.
|
||||
export const usePrefCache = () => useContext(PrefCacheContext) ?? PREF_DEFAULTS;
|
||||
@@ -49,7 +49,8 @@ import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./Rese
|
||||
import { UnauthorizedError } from "../app/errors";
|
||||
import { subscribeTopic } from "./SubscribeDialog";
|
||||
import notifier from "../app/Notifier";
|
||||
import { useCachedPref, useIsLaunchedPWA, useNotificationPermissionListener } from "./hooks";
|
||||
import { useIsLaunchedPWA, useNotificationPermissionListener } from "./hooks";
|
||||
import { usePrefCache } from "./PrefCache";
|
||||
|
||||
const maybeUpdateAccountSettings = async (payload) => {
|
||||
if (!session.exists()) {
|
||||
@@ -99,7 +100,7 @@ const Notifications = () => {
|
||||
const Sound = () => {
|
||||
const { t } = useTranslation();
|
||||
const labelId = "prefSound";
|
||||
const sound = useCachedPref(() => prefs.sound(), "pref.sound", "ding");
|
||||
const { sound } = usePrefCache();
|
||||
const handleChange = async (ev) => {
|
||||
await prefs.setSound(ev.target.value);
|
||||
await maybeUpdateAccountSettings({
|
||||
@@ -140,7 +141,7 @@ const Sound = () => {
|
||||
const MinPriority = () => {
|
||||
const { t } = useTranslation();
|
||||
const labelId = "prefMinPriority";
|
||||
const minPriority = useCachedPref(() => prefs.minPriority(), "pref.minPriority", 1);
|
||||
const { minPriority } = usePrefCache();
|
||||
const handleChange = async (ev) => {
|
||||
await prefs.setMinPriority(ev.target.value);
|
||||
await maybeUpdateAccountSettings({
|
||||
@@ -185,7 +186,7 @@ const MinPriority = () => {
|
||||
const DeleteAfter = () => {
|
||||
const { t } = useTranslation();
|
||||
const labelId = "prefDeleteAfter";
|
||||
const deleteAfter = useCachedPref(() => prefs.deleteAfter(), "pref.deleteAfter", 604800);
|
||||
const { deleteAfter } = usePrefCache();
|
||||
const handleChange = async (ev) => {
|
||||
await prefs.setDeleteAfter(ev.target.value);
|
||||
await maybeUpdateAccountSettings({
|
||||
@@ -230,7 +231,7 @@ const DeleteAfter = () => {
|
||||
const Theme = () => {
|
||||
const { t } = useTranslation();
|
||||
const labelId = "prefTheme";
|
||||
const theme = useCachedPref(() => prefs.theme(), "pref.theme", THEME.SYSTEM);
|
||||
const { theme } = usePrefCache();
|
||||
const handleChange = async (ev) => {
|
||||
await prefs.setTheme(ev.target.value);
|
||||
};
|
||||
@@ -251,7 +252,7 @@ const Theme = () => {
|
||||
const WebPushEnabled = () => {
|
||||
const { t } = useTranslation();
|
||||
const labelId = "prefWebPushEnabled";
|
||||
const enabled = useCachedPref(() => prefs.webPushEnabled(), "pref.webPushEnabled", false);
|
||||
const { webPushEnabled: enabled } = usePrefCache();
|
||||
const handleChange = async (ev) => {
|
||||
await prefs.setWebPushEnabled(ev.target.value);
|
||||
};
|
||||
|
||||
@@ -15,39 +15,6 @@ import notifier from "../app/Notifier";
|
||||
import prefs from "../app/Prefs";
|
||||
import { EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR, SW_PERIODIC_SYNC_EXTEND_TOKEN_TAG } from "../app/events";
|
||||
|
||||
/**
|
||||
* Reads a preference via a Dexie live query, but renders the last known value from localStorage
|
||||
* synchronously on first paint (and mirrors new values back to localStorage). This avoids the
|
||||
* Settings page flicker where rows "pop in" one by one while IndexedDB resolves -- the cached value
|
||||
* shows instantly and is confirmed/updated from IndexedDB in the background.
|
||||
*/
|
||||
export const useCachedPref = (reader, cacheKey, fallback) => {
|
||||
const live = useLiveQuery(reader);
|
||||
|
||||
useEffect(() => {
|
||||
if (live !== undefined && live !== null) {
|
||||
try {
|
||||
localStorage.setItem(cacheKey, JSON.stringify(live));
|
||||
} catch (e) {
|
||||
// localStorage may be unavailable (private mode); the value just isn't cached for next time
|
||||
}
|
||||
}
|
||||
}, [live, cacheKey]);
|
||||
|
||||
if (live !== undefined && live !== null) {
|
||||
return live;
|
||||
}
|
||||
try {
|
||||
const cached = localStorage.getItem(cacheKey);
|
||||
if (cached !== null) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parse/storage errors and fall back
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection
|
||||
* state changes. Conversely, when the subscription changes, the connection is refreshed (which may lead
|
||||
|
||||
Reference in New Issue
Block a user