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
WebPushExpiryDuration time.Duration
WebPushExpiryWarningDuration time.Duration
Version string // injected by App
Version string // Injected by App
}
// 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
// configuration changes for the web app version check feature.
func (c *Config) Hash() string {
data := configHashData{
BaseURL: c.BaseURL,
ListenHTTP: c.ListenHTTP,
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, err := json.Marshal(c)
if err != nil {
fmt.Println(err)
}
b, _ := json.Marshal(data)
fmt.Println(string(b))
return fmt.Sprintf("%x", sha256.Sum256(b))
}

View File

@@ -5,7 +5,8 @@
"common_back": "Back",
"common_copy_to_clipboard": "Copy to clipboard",
"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_form_username": "Username",
"signup_form_password": "Password",

View File

@@ -3,7 +3,7 @@
* 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 {
constructor() {

View File

@@ -1,18 +1,6 @@
import * as React from "react";
import { createContext, Suspense, useContext, useEffect, useState, useMemo, useCallback } from "react";
import {
Box,
Toolbar,
CssBaseline,
Backdrop,
CircularProgress,
useMediaQuery,
ThemeProvider,
createTheme,
Snackbar,
Button,
Alert,
} from "@mui/material";
import { createContext, Suspense, useContext, useEffect, useState, useMemo } from "react";
import { Box, Toolbar, CssBaseline, Backdrop, CircularProgress, useMediaQuery, ThemeProvider, createTheme } from "@mui/material";
import { useLiveQuery } from "dexie-react-hooks";
import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
@@ -26,13 +14,7 @@ import userManager from "../app/UserManager";
import { expandUrl, getKebabCaseLangStr } from "../app/utils";
import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes";
import {
useAccountListener,
useBackgroundProcesses,
useConnectionListeners,
useWebPushTopics,
useVersionChangeListener,
} from "./hooks";
import { useAccountListener, useBackgroundProcesses, useConnectionListeners, useWebPushTopics } from "./hooks";
import PublishDialog from "./PublishDialog";
import Messaging from "./Messaging";
import Login from "./Login";
@@ -118,12 +100,10 @@ const updateTitle = (newNotificationsCount) => {
};
const Layout = () => {
const { t } = useTranslation();
const params = useParams();
const { account, setAccount } = useContext(AccountContext);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
const [versionChanged, setVersionChanged] = useState(false);
const users = useLiveQuery(() => userManager.all());
const subscriptions = useLiveQuery(() => subscriptionManager.all());
const webPushTopics = useWebPushTopics();
@@ -135,18 +115,9 @@ const Layout = () => {
(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);
useAccountListener(setAccount);
useBackgroundProcesses();
useVersionChangeListener(handleVersionChange);
useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]);
return (
@@ -169,23 +140,6 @@ const Layout = () => {
/>
</Main>
<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>
);
};

View File

@@ -21,7 +21,7 @@ import {
useTheme,
} from "@mui/material";
import * as React from "react";
import { useContext, useState } from "react";
import { useCallback, useContext, useState } from "react";
import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
import Person from "@mui/icons-material/Person";
import SettingsIcon from "@mui/icons-material/Settings";
@@ -44,7 +44,7 @@ import UpgradeDialog from "./UpgradeDialog";
import { AccountContext } from "./App";
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
import { SubscriptionPopup } from "./SubscriptionPopup";
import { useNotificationPermissionListener } from "./hooks";
import { useNotificationPermissionListener, useVersionChangeListener } from "./hooks";
const navWidth = 280;
@@ -91,6 +91,13 @@ const NavList = (props) => {
const { account } = useContext(AccountContext);
const [subscribeDialogKey, setSubscribeDialogKey] = useState(0);
const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
const [versionChanged, setVersionChanged] = useState(false);
const handleVersionChange = useCallback(() => {
setVersionChanged(true);
}, []);
useVersionChangeListener(handleVersionChange);
const handleSubscribeReset = () => {
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 alertVisible =
versionChanged ||
showNotificationPermissionRequired ||
showNotificationPermissionDenied ||
showNotificationIOSInstallRequired ||
@@ -129,6 +137,7 @@ const NavList = (props) => {
<>
<Toolbar sx={{ display: { xs: "none", sm: "block" } }} />
<List component="nav" sx={{ paddingTop: { xs: 0, sm: alertVisible ? 0 : "" } }}>
{versionChanged && <VersionUpdateBanner />}
{showNotificationPermissionRequired && <NotificationPermissionRequired />}
{showNotificationPermissionDenied && <NotificationPermissionDeniedAlert />}
{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;