mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-14 07:27:55 +00:00
COndense comments, better prefcache use for theme
This commit is contained in:
@@ -48,9 +48,8 @@ const App = () => {
|
||||
document.dir = languageDir;
|
||||
}, [i18n.language, languageDir]);
|
||||
|
||||
// Keep the <html> background (visible behind the app, e.g. overscroll) in sync with the resolved
|
||||
// theme once the stored preference has loaded. The inline script in index.html sets the initial
|
||||
// class before first paint; don't override it while the preference is still loading (undefined).
|
||||
// Keep the <html> background in sync with the theme once loaded. The splash script sets the
|
||||
// initial class; don't override it while the preference is still loading.
|
||||
useEffect(() => {
|
||||
if (themePreference === undefined) {
|
||||
return;
|
||||
@@ -58,7 +57,7 @@ const App = () => {
|
||||
document.documentElement.classList.toggle("dark", isDark);
|
||||
}, [isDark, themePreference]);
|
||||
|
||||
// Safety net: never let the splash trap the UI if no route hides it (e.g. an unmatched path).
|
||||
// Safety net: hide the splash even if no route does (e.g. an unmatched path).
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => hideSplash(), 3000);
|
||||
return () => clearTimeout(timer);
|
||||
@@ -109,8 +108,7 @@ const updateTitle = (newNotificationsCount) => {
|
||||
updateFavicon(newNotificationsCount);
|
||||
};
|
||||
|
||||
// Wraps the auth pages (login, signup, ...). They render synchronously with no async data, so the
|
||||
// splash can be removed as soon as the page mounts.
|
||||
// Auth pages render synchronously, so the splash can be removed on mount.
|
||||
const AuthLayout = () => {
|
||||
useEffect(() => hideSplash(), []);
|
||||
return <Outlet />;
|
||||
@@ -123,8 +121,8 @@ const Layout = () => {
|
||||
const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
|
||||
const users = useLiveQuery(() => userManager.all());
|
||||
const subscriptions = useLiveQuery(() => subscriptionManager.all());
|
||||
// Preloaded here (not in AllSubscriptions) so the "All notifications" view has its data ready on
|
||||
// mount -- otherwise switching from a topic to All flashes an empty frame while the query runs.
|
||||
// Preloaded here so the All view (and single topics, via filter) have data on mount -- no empty
|
||||
// frame when switching.
|
||||
const allNotifications = useLiveQuery(() => subscriptionManager.getAllNotifications());
|
||||
const webPushTopics = useWebPushTopics();
|
||||
const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal);
|
||||
@@ -140,8 +138,7 @@ const Layout = () => {
|
||||
useBackgroundProcesses();
|
||||
useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]);
|
||||
|
||||
// Reveal the app only once the subscriptions have loaded from IndexedDB, so the navigation and
|
||||
// message list don't pop in empty-then-filled behind the splash.
|
||||
// Hide the splash only once subscriptions have loaded, so the nav/list don't pop in empty-then-filled.
|
||||
useEffect(() => {
|
||||
if (subscriptions !== undefined) {
|
||||
hideSplash();
|
||||
|
||||
@@ -54,8 +54,7 @@ const priorityFiles = {
|
||||
};
|
||||
|
||||
export const AllSubscriptions = () => {
|
||||
// allNotifications is preloaded in Layout (App.jsx) so this view has its data ready on mount and
|
||||
// doesn't flash an empty frame when switching to it from a topic.
|
||||
// allNotifications is preloaded in Layout, so this view has its data on mount (no empty frame on switch).
|
||||
const { subscriptions, allNotifications } = useOutletContext();
|
||||
if (!subscriptions || allNotifications === null || allNotifications === undefined) {
|
||||
return <DeferredLoading />;
|
||||
@@ -85,9 +84,8 @@ const AllSubscriptionsList = (props) => {
|
||||
|
||||
const SingleSubscriptionList = (props) => {
|
||||
const { subscription, allNotifications } = props;
|
||||
// Derived from the preloaded allNotifications by filtering -- getNotifications(id) is exactly
|
||||
// getAllNotifications() filtered by subscriptionId, so this is the same data with no per-topic
|
||||
// IndexedDB query, making switches to/between topics instant (no empty frame on mount).
|
||||
// Filter the preloaded allNotifications instead of a per-topic query (getNotifications(id) ==
|
||||
// getAllNotifications() filtered by id), so topic switches are instant.
|
||||
const notifications = useMemo(
|
||||
() => allNotifications.filter((notification) => notification.subscriptionId === subscription.id),
|
||||
[allNotifications, subscription.id]
|
||||
@@ -670,11 +668,8 @@ const Loading = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Reading notifications from IndexedDB takes only tens of milliseconds, but switching topics (or
|
||||
// going from "All notifications" to a single topic) remounts the list and briefly re-runs the
|
||||
// query, which would flash the centered Loading spinner each time. Render nothing until the load
|
||||
// has taken at least `delayMs`, so the spinner only appears for genuinely slow loads (large DB,
|
||||
// slow device) and normal switches just swap content directly.
|
||||
// Render nothing until a load takes at least `delayMs`, so the centered spinner only shows on
|
||||
// genuinely slow loads -- normal sub-frame IndexedDB reads don't flash it on every remount.
|
||||
const DeferredLoading = ({ delayMs = 250 }) => {
|
||||
const [show, setShow] = useState(false);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
import * as React from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import { createContext, useContext, useEffect } 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.
|
||||
// A CACHE of the user's prefs -- not the source of truth (that's the `prefs` IndexedDB table, via
|
||||
// Prefs.js). Preloaded once in Layout so Settings renders instantly, and written through to
|
||||
// localStorage on every change so (a) Settings is instant even on a cold load and (b) the inline
|
||||
// splash script in index.html can read the theme synchronously before the bundle loads. The
|
||||
// "prefcache" key is duplicated in that script -- keep them in sync.
|
||||
const PREFCACHE_LOCALSTORAGE_KEY = "prefcache";
|
||||
|
||||
const PrefCacheContext = createContext(undefined);
|
||||
|
||||
// Synchronous fallback before the live query resolves. Merged over PREF_DEFAULTS so a newly-added
|
||||
// pref still has a value.
|
||||
const readPersistedCache = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem(PREFCACHE_LOCALSTORAGE_KEY);
|
||||
if (raw) {
|
||||
return { ...PREF_DEFAULTS, ...JSON.parse(raw) };
|
||||
}
|
||||
} catch (e) {
|
||||
// malformed or unavailable storage -- fall back to defaults
|
||||
}
|
||||
return PREF_DEFAULTS;
|
||||
};
|
||||
|
||||
export const PrefCacheProvider = ({ children }) => {
|
||||
const cache = useLiveQuery(async () => ({
|
||||
sound: await prefs.sound(),
|
||||
@@ -18,9 +34,21 @@ export const PrefCacheProvider = ({ children }) => {
|
||||
theme: await prefs.theme(),
|
||||
webPushEnabled: await prefs.webPushEnabled(),
|
||||
}));
|
||||
|
||||
// Write through to localStorage on change (prefs change rarely).
|
||||
useEffect(() => {
|
||||
if (cache !== undefined) {
|
||||
try {
|
||||
localStorage.setItem(PREFCACHE_LOCALSTORAGE_KEY, JSON.stringify(cache));
|
||||
} catch (e) {
|
||||
// localStorage may be unavailable (private mode) -- the cache just isn't persisted
|
||||
}
|
||||
}
|
||||
}, [cache]);
|
||||
|
||||
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;
|
||||
// Live context once resolved; else the synchronous localStorage snapshot (instant on cold load);
|
||||
// else defaults.
|
||||
export const usePrefCache = () => useContext(PrefCacheContext) ?? readPersistedCache();
|
||||
|
||||
Reference in New Issue
Block a user