This commit is contained in:
binwiederhier
2026-01-18 09:44:21 -05:00
parent cc9f9c0d24
commit 5a1aa68ead
5 changed files with 38 additions and 131 deletions

View File

@@ -182,7 +182,7 @@ type Config struct {
WebPushStartupQueries string WebPushStartupQueries string
WebPushExpiryDuration time.Duration WebPushExpiryDuration time.Duration
WebPushExpiryWarningDuration time.Duration WebPushExpiryWarningDuration time.Duration
Version string // injected by App Version string // Injected by App
} }
// NewConfig instantiates a default new server config // NewConfig instantiates a default new server config
@@ -279,86 +279,13 @@ func NewConfig() *Config {
} }
} }
// configHashData is a subset of Config fields used for computing the config hash.
// It excludes sensitive fields (keys, passwords, tokens) and runtime-only fields.
type configHashData struct {
BaseURL string
ListenHTTP string
ListenHTTPS string
ListenUnix string
CacheDuration time.Duration
AttachmentTotalSizeLimit int64
AttachmentFileSizeLimit int64
AttachmentExpiryDuration time.Duration
KeepaliveInterval time.Duration
ManagerInterval time.Duration
DisallowedTopics []string
WebRoot string
MessageDelayMin time.Duration
MessageDelayMax time.Duration
MessageSizeLimit int
TotalTopicLimit int
VisitorSubscriptionLimit int
VisitorAttachmentTotalSizeLimit int64
VisitorAttachmentDailyBandwidthLimit int64
VisitorRequestLimitBurst int
VisitorRequestLimitReplenish time.Duration
VisitorMessageDailyLimit int
VisitorEmailLimitBurst int
VisitorEmailLimitReplenish time.Duration
EnableSignup bool
EnableLogin bool
RequireLogin bool
EnableReservations bool
EnableMetrics bool
EnablePayments bool
EnableCalls bool
EnableEmails bool
EnableWebPush bool
BillingContact string
Version string
}
// Hash computes a SHA-256 hash of the configuration. This is used to detect // Hash computes a SHA-256 hash of the configuration. This is used to detect
// configuration changes for the web app version check feature. // configuration changes for the web app version check feature.
func (c *Config) Hash() string { func (c *Config) Hash() string {
data := configHashData{ b, err := json.Marshal(c)
BaseURL: c.BaseURL, if err != nil {
ListenHTTP: c.ListenHTTP, fmt.Println(err)
ListenHTTPS: c.ListenHTTPS,
ListenUnix: c.ListenUnix,
CacheDuration: c.CacheDuration,
AttachmentTotalSizeLimit: c.AttachmentTotalSizeLimit,
AttachmentFileSizeLimit: c.AttachmentFileSizeLimit,
AttachmentExpiryDuration: c.AttachmentExpiryDuration,
KeepaliveInterval: c.KeepaliveInterval,
ManagerInterval: c.ManagerInterval,
DisallowedTopics: c.DisallowedTopics,
WebRoot: c.WebRoot,
MessageDelayMin: c.MessageDelayMin,
MessageDelayMax: c.MessageDelayMax,
MessageSizeLimit: c.MessageSizeLimit,
TotalTopicLimit: c.TotalTopicLimit,
VisitorSubscriptionLimit: c.VisitorSubscriptionLimit,
VisitorAttachmentTotalSizeLimit: c.VisitorAttachmentTotalSizeLimit,
VisitorAttachmentDailyBandwidthLimit: c.VisitorAttachmentDailyBandwidthLimit,
VisitorRequestLimitBurst: c.VisitorRequestLimitBurst,
VisitorRequestLimitReplenish: c.VisitorRequestLimitReplenish,
VisitorMessageDailyLimit: c.VisitorMessageDailyLimit,
VisitorEmailLimitBurst: c.VisitorEmailLimitBurst,
VisitorEmailLimitReplenish: c.VisitorEmailLimitReplenish,
EnableSignup: c.EnableSignup,
EnableLogin: c.EnableLogin,
RequireLogin: c.RequireLogin,
EnableReservations: c.EnableReservations,
EnableMetrics: c.EnableMetrics,
EnablePayments: c.StripeSecretKey != "",
EnableCalls: c.TwilioAccount != "",
EnableEmails: c.SMTPSenderFrom != "",
EnableWebPush: c.WebPushPublicKey != "",
BillingContact: c.BillingContact,
Version: c.Version,
} }
b, _ := json.Marshal(data) fmt.Println(string(b))
return fmt.Sprintf("%x", sha256.Sum256(b)) return fmt.Sprintf("%x", sha256.Sum256(b))
} }

View File

@@ -5,7 +5,8 @@
"common_back": "Back", "common_back": "Back",
"common_copy_to_clipboard": "Copy to clipboard", "common_copy_to_clipboard": "Copy to clipboard",
"common_refresh": "Refresh", "common_refresh": "Refresh",
"version_update_available": "New ntfy version available. Please refresh the page.", "version_update_available_title": "New version available",
"version_update_available_description": "The ntfy server has been updated. Please refresh the page.",
"signup_title": "Create a ntfy account", "signup_title": "Create a ntfy account",
"signup_form_username": "Username", "signup_form_username": "Username",
"signup_form_password": "Password", "signup_form_password": "Password",

View File

@@ -3,7 +3,7 @@
* or configuration changes, prompting users to refresh the page. * or configuration changes, prompting users to refresh the page.
*/ */
const CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes const CHECK_INTERVAL = 30 * 1000; // 5 * 60 * 1000; // 5 minutes
class VersionChecker { class VersionChecker {
constructor() { constructor() {

View File

@@ -1,18 +1,6 @@
import * as React from "react"; import * as React from "react";
import { createContext, Suspense, useContext, useEffect, useState, useMemo, useCallback } from "react"; import { createContext, Suspense, useContext, useEffect, useState, useMemo } from "react";
import { import { Box, Toolbar, CssBaseline, Backdrop, CircularProgress, useMediaQuery, ThemeProvider, createTheme } from "@mui/material";
Box,
Toolbar,
CssBaseline,
Backdrop,
CircularProgress,
useMediaQuery,
ThemeProvider,
createTheme,
Snackbar,
Button,
Alert,
} from "@mui/material";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -26,13 +14,7 @@ import userManager from "../app/UserManager";
import { expandUrl, getKebabCaseLangStr } from "../app/utils"; import { expandUrl, getKebabCaseLangStr } from "../app/utils";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes"; import routes from "./routes";
import { import { useAccountListener, useBackgroundProcesses, useConnectionListeners, useWebPushTopics } from "./hooks";
useAccountListener,
useBackgroundProcesses,
useConnectionListeners,
useWebPushTopics,
useVersionChangeListener,
} from "./hooks";
import PublishDialog from "./PublishDialog"; import PublishDialog from "./PublishDialog";
import Messaging from "./Messaging"; import Messaging from "./Messaging";
import Login from "./Login"; import Login from "./Login";
@@ -118,12 +100,10 @@ const updateTitle = (newNotificationsCount) => {
}; };
const Layout = () => { const Layout = () => {
const { t } = useTranslation();
const params = useParams(); const params = useParams();
const { account, setAccount } = useContext(AccountContext); const { account, setAccount } = useContext(AccountContext);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [sendDialogOpenMode, setSendDialogOpenMode] = useState(""); const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
const [versionChanged, setVersionChanged] = useState(false);
const users = useLiveQuery(() => userManager.all()); const users = useLiveQuery(() => userManager.all());
const subscriptions = useLiveQuery(() => subscriptionManager.all()); const subscriptions = useLiveQuery(() => subscriptionManager.all());
const webPushTopics = useWebPushTopics(); const webPushTopics = useWebPushTopics();
@@ -135,18 +115,9 @@ const Layout = () => {
(config.base_url === s.baseUrl && params.topic === s.topic) (config.base_url === s.baseUrl && params.topic === s.topic)
); );
const handleVersionChange = useCallback(() => {
setVersionChanged(true);
}, []);
const handleRefresh = useCallback(() => {
window.location.reload();
}, []);
useConnectionListeners(account, subscriptions, users, webPushTopics); useConnectionListeners(account, subscriptions, users, webPushTopics);
useAccountListener(setAccount); useAccountListener(setAccount);
useBackgroundProcesses(); useBackgroundProcesses();
useVersionChangeListener(handleVersionChange);
useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]);
return ( return (
@@ -169,23 +140,6 @@ const Layout = () => {
/> />
</Main> </Main>
<Messaging selected={selected} dialogOpenMode={sendDialogOpenMode} onDialogOpenModeChange={setSendDialogOpenMode} /> <Messaging selected={selected} dialogOpenMode={sendDialogOpenMode} onDialogOpenModeChange={setSendDialogOpenMode} />
<Snackbar
open={versionChanged}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
sx={{ bottom: { xs: 70, md: 24 } }}
>
<Alert
severity="info"
variant="filled"
action={
<Button color="inherit" size="small" onClick={handleRefresh}>
{t("common_refresh")}
</Button>
}
>
{t("version_update_available")}
</Alert>
</Snackbar>
</Box> </Box>
); );
}; };

View File

@@ -21,7 +21,7 @@ import {
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import * as React from "react"; import * as React from "react";
import { useContext, useState } from "react"; import { useCallback, useContext, useState } from "react";
import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline"; import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
import Person from "@mui/icons-material/Person"; import Person from "@mui/icons-material/Person";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
@@ -44,7 +44,7 @@ import UpgradeDialog from "./UpgradeDialog";
import { AccountContext } from "./App"; import { AccountContext } from "./App";
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
import { SubscriptionPopup } from "./SubscriptionPopup"; import { SubscriptionPopup } from "./SubscriptionPopup";
import { useNotificationPermissionListener } from "./hooks"; import { useNotificationPermissionListener, useVersionChangeListener } from "./hooks";
const navWidth = 280; const navWidth = 280;
@@ -91,6 +91,13 @@ const NavList = (props) => {
const { account } = useContext(AccountContext); const { account } = useContext(AccountContext);
const [subscribeDialogKey, setSubscribeDialogKey] = useState(0); const [subscribeDialogKey, setSubscribeDialogKey] = useState(0);
const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
const [versionChanged, setVersionChanged] = useState(false);
const handleVersionChange = useCallback(() => {
setVersionChanged(true);
}, []);
useVersionChangeListener(handleVersionChange);
const handleSubscribeReset = () => { const handleSubscribeReset = () => {
setSubscribeDialogOpen(false); setSubscribeDialogOpen(false);
@@ -119,6 +126,7 @@ const NavList = (props) => {
const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
const alertVisible = const alertVisible =
versionChanged ||
showNotificationPermissionRequired || showNotificationPermissionRequired ||
showNotificationPermissionDenied || showNotificationPermissionDenied ||
showNotificationIOSInstallRequired || showNotificationIOSInstallRequired ||
@@ -129,6 +137,7 @@ const NavList = (props) => {
<> <>
<Toolbar sx={{ display: { xs: "none", sm: "block" } }} /> <Toolbar sx={{ display: { xs: "none", sm: "block" } }} />
<List component="nav" sx={{ paddingTop: { xs: 0, sm: alertVisible ? 0 : "" } }}> <List component="nav" sx={{ paddingTop: { xs: 0, sm: alertVisible ? 0 : "" } }}>
{versionChanged && <VersionUpdateBanner />}
{showNotificationPermissionRequired && <NotificationPermissionRequired />} {showNotificationPermissionRequired && <NotificationPermissionRequired />}
{showNotificationPermissionDenied && <NotificationPermissionDeniedAlert />} {showNotificationPermissionDenied && <NotificationPermissionDeniedAlert />}
{showNotificationBrowserNotSupportedBox && <NotificationBrowserNotSupportedAlert />} {showNotificationBrowserNotSupportedBox && <NotificationBrowserNotSupportedAlert />}
@@ -425,4 +434,20 @@ const NotificationContextNotSupportedAlert = () => {
); );
}; };
const VersionUpdateBanner = () => {
const { t } = useTranslation();
const handleRefresh = () => {
window.location.reload();
};
return (
<Alert severity="info" sx={{ paddingTop: 2 }}>
<AlertTitle>{t("version_update_available_title")}</AlertTitle>
<Typography gutterBottom>{t("version_update_available_description")}</Typography>
<Button sx={{ float: "right" }} color="inherit" size="small" onClick={handleRefresh}>
{t("common_refresh")}
</Button>
</Alert>
);
};
export default Navigation; export default Navigation;