useContext work in JS

This commit is contained in:
binwiederhier 2023-01-09 20:37:13 -05:00
parent a4529617cc
commit b27c608508
17 changed files with 87 additions and 176 deletions

View file

@ -79,7 +79,7 @@ var flagsServe = append(
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "xxx"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "xxx"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "xxx"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "xxx"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-payments", Aliases: []string{"enable_payments"}, EnvVars: []string{"NTFY_ENABLE_PAYMENTS"}, Value: false, Usage: "xxx"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-payments", Aliases: []string{"enable_payments"}, EnvVars: []string{"NTFY_ENABLE_PAYMENTS"}, Value: false, Usage: "xxx"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reserve-topics", Aliases: []string{"enable_reserve_topics"}, EnvVars: []string{"NTFY_ENABLE_RESERVE_TOPICS"}, Value: false, Usage: "xxx"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "xxx"}),
) )
var cmdServe = &cli.Command{ var cmdServe = &cli.Command{
@ -151,7 +151,7 @@ func execServe(c *cli.Context) error {
enableSignup := c.Bool("enable-signup") enableSignup := c.Bool("enable-signup")
enableLogin := c.Bool("enable-login") enableLogin := c.Bool("enable-login")
enablePayments := c.Bool("enable-payments") enablePayments := c.Bool("enable-payments")
enableReserveTopics := c.Bool("enable-reserve-topics") enableReservations := c.Bool("enable-reservations")
// Check values // Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) { if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
@ -188,7 +188,7 @@ func execServe(c *cli.Context) error {
return errors.New("if upstream-base-url is set, base-url must also be set") return errors.New("if upstream-base-url is set, base-url must also be set")
} else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL { } else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL {
return errors.New("base-url and upstream-base-url cannot be identical, you'll likely want to set upstream-base-url to https://ntfy.sh, see https://ntfy.sh/docs/config/#ios-instant-notifications") return errors.New("base-url and upstream-base-url cannot be identical, you'll likely want to set upstream-base-url to https://ntfy.sh, see https://ntfy.sh/docs/config/#ios-instant-notifications")
} else if authFile == "" && (enableSignup || enableLogin || enableReserveTopics || enablePayments) { } else if authFile == "" && (enableSignup || enableLogin || enableReservations || enablePayments) {
return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or enable-payments if auth-file is not set") return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or enable-payments if auth-file is not set")
} }
@ -284,7 +284,7 @@ func execServe(c *cli.Context) error {
conf.EnableSignup = enableSignup conf.EnableSignup = enableSignup
conf.EnableLogin = enableLogin conf.EnableLogin = enableLogin
conf.EnablePayments = enablePayments conf.EnablePayments = enablePayments
conf.EnableReserveTopics = enableReserveTopics conf.EnableReservations = enableReservations
conf.Version = c.App.Version conf.Version = c.App.Version
// Set up hot-reloading of config // Set up hot-reloading of config

4
go.sum
View file

@ -35,8 +35,6 @@ github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8b
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8= github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -172,8 +170,6 @@ google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4Ho
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY=
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=

View file

@ -110,7 +110,7 @@ type Config struct {
EnableEmailConfirm bool EnableEmailConfirm bool
EnablePasswordReset bool EnablePasswordReset bool
EnablePayments bool EnablePayments bool
EnableReserveTopics bool // Allow users with role "user" to own/reserve topics EnableReservations bool // Allow users with role "user" to own/reserve topics
Version string // injected by App Version string // injected by App
} }

View file

@ -44,7 +44,6 @@ import (
UI: UI:
- flicker of upgrade banner - flicker of upgrade banner
- JS constants - JS constants
- useContext for account
Sync: Sync:
- "account topic" sync mechanism - "account topic" sync mechanism
- "mute" setting - "mute" setting
@ -58,9 +57,7 @@ import (
Refactor: Refactor:
- rename /access -> /reservation - rename /access -> /reservation
Later: Later:
- Password reset
- Pricing - Pricing
- change email
*/ */
// Server is the main server, providing the UI and API for ntfy // Server is the main server, providing the UI and API for ntfy
@ -457,10 +454,10 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
EnableSignup: s.config.EnableSignup, EnableSignup: s.config.EnableSignup,
EnablePasswordReset: s.config.EnablePasswordReset, EnablePasswordReset: s.config.EnablePasswordReset,
EnablePayments: s.config.EnablePayments, EnablePayments: s.config.EnablePayments,
EnableReserveTopics: s.config.EnableReserveTopics, EnableReservations: s.config.EnableReservations,
DisallowedTopics: disallowedTopics, DisallowedTopics: disallowedTopics,
} }
b, err := json.Marshal(response) b, err := json.MarshalIndent(response, "", " ")
if err != nil { if err != nil {
return err return err
} }

View file

@ -292,6 +292,6 @@ type apiConfigResponse struct {
EnableSignup bool `json:"enable_signup"` EnableSignup bool `json:"enable_signup"`
EnablePasswordReset bool `json:"enable_password_reset"` EnablePasswordReset bool `json:"enable_password_reset"`
EnablePayments bool `json:"enable_payments"` EnablePayments bool `json:"enable_payments"`
EnableReserveTopics bool `json:"enable_reserve_topics"` EnableReservations bool `json:"enable_reservations"`
DisallowedTopics []string `json:"disallowed_topics"` DisallowedTopics []string `json:"disallowed_topics"`
} }

View file

@ -12,6 +12,6 @@ var config = {
enable_signup: true, enable_signup: true,
enable_password_reset: false, enable_password_reset: false,
enable_payments: true, enable_payments: true,
enable_reserve_topics: true, enable_reservations: true,
disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "pricing", "signup", "login", "reset-password"] disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "pricing", "signup", "login", "reset-password"]
}; };

View file

@ -243,6 +243,7 @@
"prefs_appearance_language_title": "Language", "prefs_appearance_language_title": "Language",
"prefs_reservations_title": "Reserved topics", "prefs_reservations_title": "Reserved topics",
"prefs_reservations_description": "You can reserve topic names for personal use here. Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.", "prefs_reservations_description": "You can reserve topic names for personal use here. Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.",
"prefs_reservations_limit_reached": "You reached your reserved topics limit.",
"prefs_reservations_add_button": "Add reserved topic", "prefs_reservations_add_button": "Add reserved topic",
"prefs_reservations_edit_button": "Edit topic access", "prefs_reservations_edit_button": "Edit topic access",
"prefs_reservations_delete_button": "Reset topic access", "prefs_reservations_delete_button": "Reset topic access",

View file

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import {useState} from 'react'; import {useContext, useState} from 'react';
import {LinearProgress, Link, Stack, useMediaQuery} from "@mui/material"; import {LinearProgress, Stack, useMediaQuery} from "@mui/material";
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
@ -18,7 +18,6 @@ import TextField from "@mui/material/TextField";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import routes from "./routes"; import routes from "./routes";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import {useOutletContext} from "react-router-dom";
import {formatBytes} from "../app/utils"; import {formatBytes} from "../app/utils";
import accountApi, {UnauthorizedError} from "../app/AccountApi"; import accountApi, {UnauthorizedError} from "../app/AccountApi";
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
@ -28,6 +27,7 @@ import i18n from "i18next";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import UpgradeDialog from "./UpgradeDialog"; import UpgradeDialog from "./UpgradeDialog";
import CelebrationIcon from "@mui/icons-material/Celebration"; import CelebrationIcon from "@mui/icons-material/Celebration";
import {AccountContext} from "./App";
const Account = () => { const Account = () => {
if (!session.exists()) { if (!session.exists()) {
@ -62,7 +62,7 @@ const Basics = () => {
const Username = () => { const Username = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { account } = useOutletContext(); const { account } = useContext(AccountContext);
const labelId = "prefUsername"; const labelId = "prefUsername";
return ( return (
@ -169,23 +169,12 @@ const ChangePasswordDialog = (props) => {
const Stats = () => { const Stats = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { account } = useOutletContext(); const { account } = useContext(AccountContext);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
if (!account) { if (!account) {
return <></>; return <></>;
} }
const normalize = (value, max) => Math.min(value / max * 100, 100); const normalize = (value, max) => Math.min(value / max * 100, 100);
const barColor = (remaining, limit) => {
if (account.role === "admin") {
return "primary";
} else if (limit > 0 && remaining === 0) {
return "error";
}
return "primary";
};
return ( return (
<Card sx={{p: 3}} aria-label={t("account_usage_title")}> <Card sx={{p: 3}} aria-label={t("account_usage_title")}>
<Typography variant="h5" sx={{marginBottom: 2}}> <Typography variant="h5" sx={{marginBottom: 2}}>
@ -238,7 +227,6 @@ const Stats = () => {
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100} value={account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100}
color={barColor(account.stats.reservations_remaining, account.limits.reservations)}
/> />
</> </>
} }
@ -260,7 +248,6 @@ const Stats = () => {
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={account.role === "user" ? normalize(account.stats.messages, account.limits.messages) : 100} value={account.role === "user" ? normalize(account.stats.messages, account.limits.messages) : 100}
color={account.role === "user" && account.stats.messages_remaining === 0 ? 'error' : 'primary'}
/> />
</Pref> </Pref>
<Pref title={ <Pref title={
@ -271,12 +258,11 @@ const Stats = () => {
}> }>
<div> <div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography> <Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography> <Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
</div> </div>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100} value={account.role === "user" ? normalize(account.stats.emails, account.limits.emails) : 100}
color={account?.role !== "admin" && account.stats.emails_remaining === 0 ? 'error' : 'primary'}
/> />
</Pref> </Pref>
<Pref <Pref
@ -292,16 +278,15 @@ const Stats = () => {
> >
<div> <div>
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography> <Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography> <Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography>
</div> </div>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} value={account.role === "user" ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100}
color={account.role !== "admin" && account.stats.attachment_total_size_remaining === 0 ? 'error' : 'primary'}
/> />
</Pref> </Pref>
</PrefGroup> </PrefGroup>
{account.limits.basis === "ip" && {account.role === "user" && account.limits.basis === "ip" &&
<Typography variant="body1"> <Typography variant="body1">
{t("account_usage_basis_ip_description")} {t("account_usage_basis_ip_description")}
</Typography> </Typography>

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import {Suspense, useEffect, useState} from 'react'; import {createContext, Suspense, useContext, useEffect, useState} from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import {ThemeProvider} from '@mui/material/styles'; import {ThemeProvider} from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
import Notifications from "./Notifications"; import {AllSubscriptions, SingleSubscription} from "./Notifications";
import theme from "./theme"; import theme from "./theme";
import Navigation from "./Navigation"; import Navigation from "./Navigation";
import ActionBar from "./ActionBar"; import ActionBar from "./ActionBar";
@ -13,11 +13,11 @@ import Preferences from "./Preferences";
import {useLiveQuery} from "dexie-react-hooks"; import {useLiveQuery} from "dexie-react-hooks";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from "react-router-dom"; import {BrowserRouter, Outlet, Route, Routes, useParams} from "react-router-dom";
import {expandUrl} from "../app/utils"; import {expandUrl} from "../app/utils";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes"; import routes from "./routes";
import {useAccountListener, useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; import {useAccountListener, useBackgroundProcesses, useConnectionListeners} from "./hooks";
import PublishDialog from "./PublishDialog"; import PublishDialog from "./PublishDialog";
import Messaging from "./Messaging"; import Messaging from "./Messaging";
import "./i18n"; // Translations! import "./i18n"; // Translations!
@ -27,53 +27,45 @@ import Login from "./Login";
import Pricing from "./Pricing"; import Pricing from "./Pricing";
import Signup from "./Signup"; import Signup from "./Signup";
import Account from "./Account"; import Account from "./Account";
import ResetPassword from "./ResetPassword";
export const AccountContext = createContext(null);
const App = () => { const App = () => {
const [account, setAccount] = useState(null);
return ( return (
<Suspense fallback={<Loader />}> <Suspense fallback={<Loader />}>
<BrowserRouter> <BrowserRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline/> <AccountContext.Provider value={{ account, setAccount }}>
<ErrorBoundary> <CssBaseline/>
<Routes> <ErrorBoundary>
<Route path={routes.home} element={<Home/>}/> <Routes>
<Route path={routes.pricing} element={<Pricing/>}/> <Route path={routes.home} element={<Home/>}/>
<Route path={routes.login} element={<Login/>}/> <Route path={routes.pricing} element={<Pricing/>}/>
<Route path={routes.signup} element={<Signup/>}/> <Route path={routes.login} element={<Login/>}/>
<Route path={routes.resetPassword} element={<ResetPassword/>}/> <Route path={routes.signup} element={<Signup/>}/>
<Route element={<Layout/>}> <Route element={<Layout/>}>
<Route path={routes.app} element={<AllSubscriptions/>}/> <Route path={routes.app} element={<AllSubscriptions/>}/>
<Route path={routes.account} element={<Account/>}/> <Route path={routes.account} element={<Account/>}/>
<Route path={routes.settings} element={<Preferences/>}/> <Route path={routes.settings} element={<Preferences/>}/>
<Route path={routes.subscription} element={<SingleSubscription/>}/> <Route path={routes.subscription} element={<SingleSubscription/>}/>
<Route path={routes.subscriptionExternal} element={<SingleSubscription/>}/> <Route path={routes.subscriptionExternal} element={<SingleSubscription/>}/>
</Route> </Route>
</Routes> </Routes>
</ErrorBoundary> </ErrorBoundary>
</AccountContext.Provider>
</ThemeProvider> </ThemeProvider>
</BrowserRouter> </BrowserRouter>
</Suspense> </Suspense>
); );
} }
const AllSubscriptions = () => {
const { subscriptions } = useOutletContext();
return <Notifications mode="all" subscriptions={subscriptions}/>;
};
const SingleSubscription = () => {
const { subscriptions, selected } = useOutletContext();
useAutoSubscribe(subscriptions, selected);
return <Notifications mode="one" subscription={selected}/>;
};
const Layout = () => { const Layout = () => {
const params = useParams(); const params = useParams();
const { account, setAccount } = useContext(AccountContext);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted()); const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted());
const [sendDialogOpenMode, setSendDialogOpenMode] = useState(""); const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
const [account, setAccount] = useState(null);
const users = useLiveQuery(() => userManager.all()); const users = useLiveQuery(() => userManager.all());
const subscriptions = useLiveQuery(() => subscriptionManager.all()); const subscriptions = useLiveQuery(() => subscriptionManager.all());
const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0; const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0;
@ -94,7 +86,6 @@ const Layout = () => {
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
/> />
<Navigation <Navigation
account={account}
subscriptions={subscriptions} subscriptions={subscriptions}
selectedSubscription={selected} selectedSubscription={selected}
notificationsGranted={notificationsGranted} notificationsGranted={notificationsGranted}
@ -105,7 +96,7 @@ const Layout = () => {
/> />
<Main> <Main>
<Toolbar/> <Toolbar/>
<Outlet context={{ account, subscriptions, selected }}/> <Outlet context={{ subscriptions, selected }}/>
</Main> </Main>
<Messaging <Messaging
selected={selected} selected={selected}

View file

@ -1,6 +1,6 @@
import Drawer from "@mui/material/Drawer"; import Drawer from "@mui/material/Drawer";
import * as React from "react"; import * as React from "react";
import {useState} from "react"; import {useContext, useState} from "react";
import ListItemButton from "@mui/material/ListItemButton"; import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline"; import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
@ -30,6 +30,7 @@ import session from "../app/Session";
import accountApi from "../app/AccountApi"; import accountApi from "../app/AccountApi";
import CelebrationIcon from '@mui/icons-material/Celebration'; import CelebrationIcon from '@mui/icons-material/Celebration';
import UpgradeDialog from "./UpgradeDialog"; import UpgradeDialog from "./UpgradeDialog";
import {AccountContext} from "./App";
const navWidth = 280; const navWidth = 280;
@ -76,6 +77,7 @@ const NavList = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { account } = useContext(AccountContext);
const [subscribeDialogKey, setSubscribeDialogKey] = useState(0); const [subscribeDialogKey, setSubscribeDialogKey] = useState(0);
const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
@ -100,8 +102,8 @@ const NavList = (props) => {
navigate(routes.account); navigate(routes.account);
}; };
const isAdmin = props.account?.role === "admin"; const isAdmin = account?.role === "admin";
const isPaid = props.account?.tier?.paid; const isPaid = account?.tier?.paid;
const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid;// && (!props.account || !props.account.tier || !props.account.tier.paid || props.account); const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid;// && (!props.account || !props.account.tier || !props.account.tier.paid || props.account);
const showSubscriptionsList = props.subscriptions?.length > 0; const showSubscriptionsList = props.subscriptions?.length > 0;
const showNotificationBrowserNotSupportedBox = !notifier.browserSupported(); const showNotificationBrowserNotSupportedBox = !notifier.browserSupported();

View file

@ -19,7 +19,8 @@ import {
formatBytes, formatBytes,
formatMessage, formatMessage,
formatShortDateTime, formatShortDateTime,
formatTitle, maybeAppendActionErrors, formatTitle,
maybeAppendActionErrors,
openUrl, openUrl,
shortUrl, shortUrl,
topicShortUrl, topicShortUrl,
@ -41,15 +42,27 @@ import priority5 from "../img/priority-5.svg";
import logoOutline from "../img/ntfy-outline.svg"; import logoOutline from "../img/ntfy-outline.svg";
import AttachmentIcon from "./AttachmentIcon"; import AttachmentIcon from "./AttachmentIcon";
import {Trans, useTranslation} from "react-i18next"; import {Trans, useTranslation} from "react-i18next";
import {useOutletContext} from "react-router-dom";
import {useAutoSubscribe} from "./hooks";
const Notifications = (props) => { export const AllSubscriptions = () => {
if (props.mode === "all") { const { subscriptions } = useOutletContext();
return (props.subscriptions) ? <AllSubscriptions subscriptions={props.subscriptions}/> : <Loading/>; if (!subscriptions) {
return <Loading/>;
} }
return (props.subscription) ? <SingleSubscription subscription={props.subscription}/> : <Loading/>; return <AllSubscriptionsList subscriptions={subscriptions}/>;
} };
const AllSubscriptions = (props) => { export const SingleSubscription = () => {
const { subscriptions, selected } = useOutletContext();
useAutoSubscribe(subscriptions, selected);
if (!selected) {
return <Loading/>;
}
return <SingleSubscriptionList subscription={selected}/>;
};
const AllSubscriptionsList = (props) => {
const subscriptions = props.subscriptions; const subscriptions = props.subscriptions;
const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []);
if (notifications === null || notifications === undefined) { if (notifications === null || notifications === undefined) {
@ -62,7 +75,7 @@ const AllSubscriptions = (props) => {
return <NotificationList key="all" notifications={notifications} messageBar={false}/>; return <NotificationList key="all" notifications={notifications} messageBar={false}/>;
} }
const SingleSubscription = (props) => { const SingleSubscriptionList = (props) => {
const subscription = props.subscription; const subscription = props.subscription;
const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]);
if (notifications === null || notifications === undefined) { if (notifications === null || notifications === undefined) {
@ -533,5 +546,3 @@ const Loading = () => {
</VerticallyCenteredContainer> </VerticallyCenteredContainer>
); );
}; };
export default Notifications;

View file

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import {useEffect, useState} from 'react'; import {useContext, useEffect, useState} from 'react';
import { import {
Alert, Alert,
CardActions, CardActions,
@ -40,13 +40,11 @@ import session from "../app/Session";
import routes from "./routes"; import routes from "./routes";
import accountApi, {UnauthorizedError} from "../app/AccountApi"; import accountApi, {UnauthorizedError} from "../app/AccountApi";
import {Pref, PrefGroup} from "./Pref"; import {Pref, PrefGroup} from "./Pref";
import {useOutletContext} from "react-router-dom";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import {Public, PublicOff} from "@mui/icons-material"; import {Public, PublicOff} from "@mui/icons-material";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import ReserveTopicSelect from "./ReserveTopicSelect"; import ReserveTopicSelect from "./ReserveTopicSelect";
import {AccountContext} from "./App";
const Preferences = () => { const Preferences = () => {
return ( return (
@ -481,11 +479,11 @@ const Language = () => {
const Reservations = () => { const Reservations = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { account } = useOutletContext(); const { account } = useContext(AccountContext);
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
if (!config.enable_reserve_topics || !session.exists() || !account || account.role === "admin") { if (!config.enable_reservations || !session.exists() || !account || account.role === "admin") {
return <></>; return <></>;
} }
const reservations = account.reservations || []; const reservations = account.reservations || [];
@ -522,14 +520,7 @@ const Reservations = () => {
{t("prefs_reservations_description")} {t("prefs_reservations_description")}
</Paragraph> </Paragraph>
{reservations.length > 0 && <ReservationsTable reservations={reservations}/>} {reservations.length > 0 && <ReservationsTable reservations={reservations}/>}
{limitReached && {limitReached && <Alert severity="info">{t("prefs_reservations_limit_reached")}</Alert>}
<Alert severity="info">
You reached your reserved topics limit.
{config.enable_payments &&
<>{" "}<b>Upgrade</b></>
}
</Alert>
}
</CardContent> </CardContent>
<CardActions> <CardActions>
<Button onClick={handleAddClick} disabled={limitReached}>{t("prefs_reservations_add_button")}</Button> <Button onClick={handleAddClick} disabled={limitReached}>{t("prefs_reservations_add_button")}</Button>

View file

@ -1,48 +0,0 @@
import * as React from 'react';
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import routes from "./routes";
import Typography from "@mui/material/Typography";
import {NavLink} from "react-router-dom";
import AvatarBox from "./AvatarBox";
const ResetPassword = () => {
const handleSubmit = async (event) => {
//
};
return (
<AvatarBox>
<Typography sx={{ typography: 'h6' }}>
Reset password
</Typography>
<Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1, maxWidth: 400}}>
<TextField
margin="dense"
required
fullWidth
id="email"
label="Email"
name="email"
autoFocus
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{mt: 2, mb: 2}}
>
Reset password
</Button>
</Box>
<Typography sx={{mb: 4}}>
<NavLink to={routes.login} variant="body1">
&lt; Return to sign in
</NavLink>
</Typography>
</AvatarBox>
);
}
export default ResetPassword;

View file

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import {useState} from 'react'; import {useContext, useState} from 'react';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
@ -19,7 +19,7 @@ import session from "../app/Session";
import routes from "./routes"; import routes from "./routes";
import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi"; import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi";
import ReserveTopicSelect from "./ReserveTopicSelect"; import ReserveTopicSelect from "./ReserveTopicSelect";
import {useOutletContext} from "react-router-dom"; import {AccountContext} from "./App";
const publicBaseUrl = "https://ntfy.sh"; const publicBaseUrl = "https://ntfy.sh";
@ -76,7 +76,7 @@ const SubscribeDialog = (props) => {
const SubscribePage = (props) => { const SubscribePage = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
//const { account } = useOutletContext(); const { account } = useContext(AccountContext);
const [reserveTopicVisible, setReserveTopicVisible] = useState(false); const [reserveTopicVisible, setReserveTopicVisible] = useState(false);
const [anotherServerVisible, setAnotherServerVisible] = useState(false); const [anotherServerVisible, setAnotherServerVisible] = useState(false);
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
@ -87,7 +87,7 @@ const SubscribePage = (props) => {
const existingBaseUrls = Array const existingBaseUrls = Array
.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)])) .from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
.filter(s => s !== config.base_url); .filter(s => s !== config.base_url);
//const reserveTopicEnabled = session.exists() && (account?.stats.reservations_remaining || 0) > 0; const reserveTopicEnabled = session.exists() && account?.role === "user" && (account?.stats.reservations_remaining || 0) > 0;
const handleSubscribe = async () => { const handleSubscribe = async () => {
const user = await userManager.get(baseUrl); // May be undefined const user = await userManager.get(baseUrl); // May be undefined
@ -177,14 +177,14 @@ const SubscribePage = (props) => {
{t("subscribe_dialog_subscribe_button_generate_topic_name")} {t("subscribe_dialog_subscribe_button_generate_topic_name")}
</Button> </Button>
</div> </div>
{config.enable_reserve_topics && session.exists() && !anotherServerVisible && {config.enable_reservations && session.exists() && !anotherServerVisible &&
<FormGroup> <FormGroup>
<FormControlLabel <FormControlLabel
variant="standard" variant="standard"
control={ control={
<Checkbox <Checkbox
fullWidth fullWidth
// disabled={account.stats.reservations_remaining} disabled={!reserveTopicEnabled}
checked={reserveTopicVisible} checked={reserveTopicVisible}
onChange={(ev) => setReserveTopicVisible(ev.target.checked)} onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
inputProps={{ inputProps={{

View file

@ -78,7 +78,7 @@ const SubscriptionSettingsDialog = (props) => {
"aria-label": t("subscription_settings_dialog_display_name_placeholder") "aria-label": t("subscription_settings_dialog_display_name_placeholder")
}} }}
/> />
{config.enable_reserve_topics && session.exists() && {config.enable_reservations && session.exists() &&
<> <>
<FormControlLabel <FormControlLabel
fullWidth fullWidth

View file

@ -1,25 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import {useState} from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import {Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery} from "@mui/material"; import {useMediaQuery} from "@mui/material";
import theme from "./theme"; import theme from "./theme";
import api from "../app/Api";
import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
import userManager from "../app/UserManager";
import subscriptionManager from "../app/SubscriptionManager";
import poller from "../app/Poller";
import DialogFooter from "./DialogFooter"; import DialogFooter from "./DialogFooter";
import {useTranslation} from "react-i18next";
import session from "../app/Session";
import routes from "./routes";
import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi";
import ReserveTopicSelect from "./ReserveTopicSelect";
import {useOutletContext} from "react-router-dom";
const UpgradeDialog = (props) => { const UpgradeDialog = (props) => {
const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));

View file

@ -8,7 +8,7 @@ const routes = {
pricing: "/pricing", pricing: "/pricing",
login: "/login", login: "/login",
signup: "/signup", signup: "/signup",
resetPassword: "/reset-password", resetPassword: "/reset-password", // Not used (yet)
app: config.app_root, app: config.app_root,
account: "/account", account: "/account",
settings: "/settings", settings: "/settings",