Răsfoiți Sursa

Upgrade banner

binwiederhier 3 ani în urmă
părinte
comite
3280c2c440

+ 2 - 2
server/config.go

@@ -107,8 +107,8 @@ type Config struct {
 	EnableSignup                         bool
 	EnableLogin                          bool
 	EnableEmailConfirm                   bool
-	EnableResetPassword                  bool
-	EnableAccountUpgrades                bool
+	EnablePasswordReset                  bool
+	EnablePayments                       bool
 	Version                              string // injected by App
 }
 

+ 1 - 1
server/server.go

@@ -452,7 +452,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
 		AppRoot:             appRoot,
 		EnableLogin:         s.config.EnableLogin,
 		EnableSignup:        s.config.EnableSignup,
-		EnableResetPassword: s.config.EnableResetPassword,
+		EnablePasswordReset: s.config.EnablePasswordReset,
 		DisallowedTopics:    disallowedTopics,
 	}
 	b, err := json.Marshal(response)

+ 8 - 8
server/server_account.go

@@ -80,18 +80,18 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
 		}
 		if v.user.Plan != nil {
 			response.Plan = &apiAccountPlan{
-				Code:       v.user.Plan.Code,
-				Upgradable: v.user.Plan.Upgradable,
+				Code:        v.user.Plan.Code,
+				Upgradeable: v.user.Plan.Upgradeable,
 			}
 		} else if v.user.Role == user.RoleAdmin {
 			response.Plan = &apiAccountPlan{
-				Code:       string(user.PlanUnlimited),
-				Upgradable: false,
+				Code:        string(user.PlanUnlimited),
+				Upgradeable: false,
 			}
 		} else {
 			response.Plan = &apiAccountPlan{
-				Code:       string(user.PlanDefault),
-				Upgradable: true,
+				Code:        string(user.PlanDefault),
+				Upgradeable: true,
 			}
 		}
 		reservations, err := s.userManager.Reservations(v.user.Name)
@@ -111,8 +111,8 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
 		response.Username = user.Everyone
 		response.Role = string(user.RoleAnonymous)
 		response.Plan = &apiAccountPlan{
-			Code:       string(user.PlanNone),
-			Upgradable: true,
+			Code:        string(user.PlanNone),
+			Upgradeable: true,
 		}
 	}
 	w.Header().Set("Content-Type", "application/json")

+ 4 - 3
server/types.go

@@ -235,8 +235,8 @@ type apiAccountTokenResponse struct {
 }
 
 type apiAccountPlan struct {
-	Code       string `json:"code"`
-	Upgradable bool   `json:"upgradable"`
+	Code        string `json:"code"`
+	Upgradeable bool   `json:"upgradeable"`
 }
 
 type apiAccountLimits struct {
@@ -286,6 +286,7 @@ type apiConfigResponse struct {
 	AppRoot             string   `json:"app_root"`
 	EnableLogin         bool     `json:"enable_login"`
 	EnableSignup        bool     `json:"enable_signup"`
-	EnableResetPassword bool     `json:"enable_reset_password"`
+	EnablePasswordReset bool     `json:"enable_password_reset"`
+	EnablePayments      bool     `json:"enable_payments"`
 	DisallowedTopics    []string `json:"disallowed_topics"`
 }

+ 1 - 1
user/manager.go

@@ -503,7 +503,7 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
 	if planCode.Valid {
 		user.Plan = &Plan{
 			Code:                     planCode.String,
-			Upgradable:               true, // FIXME
+			Upgradeable:              false,
 			MessagesLimit:            messagesLimit.Int64,
 			EmailsLimit:              emailsLimit.Int64,
 			TopicsLimit:              topicsLimit.Int64,

+ 1 - 1
user/types.go

@@ -56,7 +56,7 @@ const (
 // Plan represents a user's account type, including its account limits
 type Plan struct {
 	Code                     string `json:"name"`
-	Upgradable               bool   `json:"upgradable"`
+	Upgradeable              bool   `json:"upgradeable"`
 	MessagesLimit            int64  `json:"messages_limit"`
 	EmailsLimit              int64  `json:"emails_limit"`
 	TopicsLimit              int64  `json:"topics_limit"`

+ 7 - 6
web/public/config.js

@@ -6,10 +6,11 @@
 // During web development, you may change values here for rapid testing.
 
 var config = {
-    baseUrl: "http://localhost:2586", // window.location.origin FIXME update before merging
-    appRoot: "/app",
-    enableLogin: true,
-    enableSignup: true,
-    enableResetPassword: false,
-    disallowedTopics: ["docs", "static", "file", "app", "account", "settings", "pricing", "signup", "login", "reset-password"]
+    base_url: "http://localhost:2586", // window.location.origin FIXME update before merging
+    app_root: "/app",
+    enable_login: true,
+    enable_signup: true,
+    enable_password_reset: false,
+    enable_payments: true,
+    disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "pricing", "signup", "login", "reset-password"]
 };

+ 1 - 0
web/public/static/langs/en.json

@@ -16,6 +16,7 @@
   "action_bar_show_menu": "Show menu",
   "action_bar_logo_alt": "ntfy logo",
   "action_bar_settings": "Settings",
+  "action_bar_account": "Account",
   "action_bar_subscription_settings": "Subscription settings",
   "action_bar_send_test_notification": "Send test notification",
   "action_bar_clear_notifications": "Clear all notifications",

+ 13 - 13
web/src/app/AccountApi.js

@@ -34,7 +34,7 @@ class AccountApi {
     }
 
     async login(user) {
-        const url = accountTokenUrl(config.baseUrl);
+        const url = accountTokenUrl(config.base_url);
         console.log(`[AccountApi] Checking auth for ${url}`);
         const response = await fetch(url, {
             method: "POST",
@@ -53,7 +53,7 @@ class AccountApi {
     }
 
     async logout() {
-        const url = accountTokenUrl(config.baseUrl);
+        const url = accountTokenUrl(config.base_url);
         console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`);
         const response = await fetch(url, {
             method: "DELETE",
@@ -67,7 +67,7 @@ class AccountApi {
     }
 
     async create(username, password) {
-        const url = accountUrl(config.baseUrl);
+        const url = accountUrl(config.base_url);
         const body = JSON.stringify({
             username: username,
             password: password
@@ -87,7 +87,7 @@ class AccountApi {
     }
 
     async get() {
-        const url = accountUrl(config.baseUrl);
+        const url = accountUrl(config.base_url);
         console.log(`[AccountApi] Fetching user account ${url}`);
         const response = await fetch(url, {
             headers: withBearerAuth({}, session.token())
@@ -106,7 +106,7 @@ class AccountApi {
     }
 
     async delete() {
-        const url = accountUrl(config.baseUrl);
+        const url = accountUrl(config.base_url);
         console.log(`[AccountApi] Deleting user account ${url}`);
         const response = await fetch(url, {
             method: "DELETE",
@@ -120,7 +120,7 @@ class AccountApi {
     }
 
     async changePassword(newPassword) {
-        const url = accountPasswordUrl(config.baseUrl);
+        const url = accountPasswordUrl(config.base_url);
         console.log(`[AccountApi] Changing account password ${url}`);
         const response = await fetch(url, {
             method: "POST",
@@ -137,7 +137,7 @@ class AccountApi {
     }
 
     async extendToken() {
-        const url = accountTokenUrl(config.baseUrl);
+        const url = accountTokenUrl(config.base_url);
         console.log(`[AccountApi] Extending user access token ${url}`);
         const response = await fetch(url, {
             method: "PATCH",
@@ -151,7 +151,7 @@ class AccountApi {
     }
 
     async updateSettings(payload) {
-        const url = accountSettingsUrl(config.baseUrl);
+        const url = accountSettingsUrl(config.base_url);
         const body = JSON.stringify(payload);
         console.log(`[AccountApi] Updating user account ${url}: ${body}`);
         const response = await fetch(url, {
@@ -167,7 +167,7 @@ class AccountApi {
     }
 
     async addSubscription(payload) {
-        const url = accountSubscriptionUrl(config.baseUrl);
+        const url = accountSubscriptionUrl(config.base_url);
         const body = JSON.stringify(payload);
         console.log(`[AccountApi] Adding user subscription ${url}: ${body}`);
         const response = await fetch(url, {
@@ -186,7 +186,7 @@ class AccountApi {
     }
 
     async updateSubscription(remoteId, payload) {
-        const url = accountSubscriptionSingleUrl(config.baseUrl, remoteId);
+        const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
         const body = JSON.stringify(payload);
         console.log(`[AccountApi] Updating user subscription ${url}: ${body}`);
         const response = await fetch(url, {
@@ -205,7 +205,7 @@ class AccountApi {
     }
 
     async deleteSubscription(remoteId) {
-        const url = accountSubscriptionSingleUrl(config.baseUrl, remoteId);
+        const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
         console.log(`[AccountApi] Removing user subscription ${url}`);
         const response = await fetch(url, {
             method: "DELETE",
@@ -219,7 +219,7 @@ class AccountApi {
     }
 
     async upsertAccess(topic, everyone) {
-        const url = accountAccessUrl(config.baseUrl);
+        const url = accountAccessUrl(config.base_url);
         console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`);
         const response = await fetch(url, {
             method: "POST",
@@ -239,7 +239,7 @@ class AccountApi {
     }
 
     async deleteAccess(topic) {
-        const url = accountAccessSingleUrl(config.baseUrl, topic);
+        const url = accountAccessSingleUrl(config.base_url, topic);
         console.log(`[AccountApi] Removing topic reservation ${url}`);
         const response = await fetch(url, {
             method: "DELETE",

+ 1 - 1
web/src/app/SubscriptionManager.js

@@ -43,7 +43,7 @@ class SubscriptionManager {
         for (let i = 0; i < remoteSubscriptions.length; i++) {
             const remote = remoteSubscriptions[i];
             const local = await this.add(remote.base_url, remote.topic);
-            const reservation = remoteReservations?.find(r => remote.base_url === config.baseUrl && remote.topic === r.topic) || null;
+            const reservation = remoteReservations?.find(r => remote.base_url === config.base_url && remote.topic === r.topic) || null;
             await this.setRemoteId(local.id, remote.id);
             await this.setDisplayName(local.id, remote.display_name);
             await this.setReservation(local.id, reservation); // May be null!

+ 4 - 4
web/src/app/UserManager.js

@@ -11,21 +11,21 @@ class UserManager {
     }
 
     async get(baseUrl) {
-        if (session.exists() && baseUrl === config.baseUrl) {
+        if (session.exists() && baseUrl === config.base_url) {
             return this.localUser();
         }
         return db.users.get(baseUrl);
     }
 
     async save(user) {
-        if (session.exists() && user.baseUrl === config.baseUrl) {
+        if (session.exists() && user.baseUrl === config.base_url) {
             return;
         }
         await db.users.put(user);
     }
 
     async delete(baseUrl) {
-        if (session.exists() && baseUrl === config.baseUrl) {
+        if (session.exists() && baseUrl === config.base_url) {
             return;
         }
         await db.users.delete(baseUrl);
@@ -36,7 +36,7 @@ class UserManager {
             return null;
         }
         return {
-            baseUrl: config.baseUrl,
+            baseUrl: config.base_url,
             username: session.username(),
             token: session.token() // Not "password"!
         };

+ 2 - 2
web/src/app/utils.js

@@ -42,13 +42,13 @@ export const validTopic = (topic) => {
 }
 
 export const disallowedTopic = (topic) => {
-    return config.disallowedTopics.includes(topic);
+    return config.disallowed_topics.includes(topic);
 }
 
 export const topicDisplayName = (subscription) => {
     if (subscription.displayName) {
         return subscription.displayName;
-    } else if (subscription.baseUrl === config.baseUrl) {
+    } else if (subscription.baseUrl === config.base_url) {
         return subscription.topic;
     }
     return topicShortUrl(subscription.baseUrl, subscription.topic);

+ 7 - 10
web/src/components/ActionBar.js

@@ -5,17 +5,12 @@ import IconButton from "@mui/material/IconButton";
 import MenuIcon from "@mui/icons-material/Menu";
 import Typography from "@mui/material/Typography";
 import * as React from "react";
-import {useEffect, useRef, useState} from "react";
+import {useState} from "react";
 import Box from "@mui/material/Box";
 import {formatShortDateTime, shuffle, topicDisplayName} from "../app/utils";
 import db from "../app/db";
 import {useLocation, useNavigate} from "react-router-dom";
-import ClickAwayListener from '@mui/material/ClickAwayListener';
-import Grow from '@mui/material/Grow';
-import Paper from '@mui/material/Paper';
-import Popper from '@mui/material/Popper';
 import MenuItem from '@mui/material/MenuItem';
-import MenuList from '@mui/material/MenuList';
 import MoreVertIcon from "@mui/icons-material/MoreVert";
 import NotificationsIcon from '@mui/icons-material/Notifications';
 import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
@@ -24,7 +19,7 @@ import routes from "./routes";
 import subscriptionManager from "../app/SubscriptionManager";
 import logo from "../img/ntfy.svg";
 import {useTranslation} from "react-i18next";
-import {Menu, Portal, Snackbar} from "@mui/material";
+import {Portal, Snackbar} from "@mui/material";
 import SubscriptionSettingsDialog from "./SubscriptionSettingsDialog";
 import session from "../app/Session";
 import AccountCircleIcon from '@mui/icons-material/AccountCircle';
@@ -41,8 +36,10 @@ const ActionBar = (props) => {
     let title = "ntfy";
     if (props.selected) {
         title = topicDisplayName(props.selected);
-    } else if (location.pathname === "/settings") {
+    } else if (location.pathname === routes.settings) {
         title = t("action_bar_settings");
+    } else if (location.pathname === routes.account) {
+        title = t("action_bar_account");
     }
     return (
         <AppBar position="fixed" sx={{
@@ -250,12 +247,12 @@ const ProfileIcon = () => {
                     <AccountCircleIcon/>
                 </IconButton>
             }
-            {!session.exists() && config.enableLogin &&
+            {!session.exists() && config.enable_login &&
                 <Button color="inherit" variant="text" onClick={() => navigate(routes.login)} sx={{m: 1}} aria-label={t("action_bar_sign_in")}>
                     {t("action_bar_sign_in")}
                 </Button>
             }
-            {!session.exists() && config.enableSignup &&
+            {!session.exists() && config.enable_signup &&
                 <Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)} aria-label={t("action_bar_sign_up")}>
                     {t("action_bar_sign_up")}
                 </Button>

+ 2 - 1
web/src/components/App.js

@@ -79,7 +79,7 @@ const Layout = () => {
     const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0;
     const [selected] = (subscriptions || []).filter(s => {
         return (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic)
-            || (config.baseUrl === s.baseUrl && params.topic === s.topic)
+            || (config.base_url === s.baseUrl && params.topic === s.topic)
     });
 
     useConnectionListeners(subscriptions, users);
@@ -95,6 +95,7 @@ const Layout = () => {
                 onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
             />
             <Navigation
+                account={account}
                 subscriptions={subscriptions}
                 selectedSubscription={selected}
                 notificationsGranted={notificationsGranted}

+ 3 - 3
web/src/components/Login.js

@@ -41,7 +41,7 @@ const Login = () => {
             }
         }
     };
-    if (!config.enableLogin) {
+    if (!config.enable_login) {
         return (
             <AvatarBox>
                 <Typography sx={{ typography: 'h6' }}>{t("Login is disabled")}</Typography>
@@ -112,8 +112,8 @@ const Login = () => {
                     </Box>
                 }
                 <Box sx={{width: "100%"}}>
-                    {config.enableResetPassword && <div style={{float: "left"}}><NavLink to={routes.resetPassword} variant="body1">{t("Reset password")}</NavLink></div>}
-                    {config.enableSignup && <div style={{float: "right"}}><NavLink to={routes.signup} variant="body1">{t("login_link_signup")}</NavLink></div>}
+                    {config.enable_password_reset && <div style={{float: "left"}}><NavLink to={routes.resetPassword} variant="body1">{t("Reset password")}</NavLink></div>}
+                    {config.enable_signup && <div style={{float: "right"}}><NavLink to={routes.signup} variant="body1">{t("login_link_signup")}</NavLink></div>}
                 </Box>
             </Box>
         </AvatarBox>

+ 1 - 1
web/src/components/Messaging.js

@@ -38,7 +38,7 @@ const Messaging = (props) => {
             <PublishDialog
                 key={`publishDialog${dialogKey}`} // Resets dialog when canceled/closed
                 openMode={dialogOpenMode}
-                baseUrl={subscription?.baseUrl ?? config.baseUrl}
+                baseUrl={subscription?.baseUrl ?? config.base_url}
                 topic={subscription?.topic ?? ""}
                 message={message}
                 onClose={handleDialogClose}

+ 35 - 16
web/src/components/Navigation.js

@@ -12,24 +12,15 @@ import List from "@mui/material/List";
 import SettingsIcon from "@mui/icons-material/Settings";
 import AddIcon from "@mui/icons-material/Add";
 import SubscribeDialog from "./SubscribeDialog";
-import {
-    Alert,
-    AlertTitle,
-    Badge,
-    CircularProgress,
-    Link,
-    ListItem,
-    ListItemSecondaryAction,
-    ListSubheader, Tooltip
-} from "@mui/material";
+import {Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Tooltip} from "@mui/material";
 import Button from "@mui/material/Button";
 import Typography from "@mui/material/Typography";
 import {openUrl, topicDisplayName, topicUrl} from "../app/utils";
 import routes from "./routes";
 import {ConnectionState} from "../app/Connection";
-import {useLocation, useNavigate, useOutletContext} from "react-router-dom";
+import {useLocation, useNavigate} from "react-router-dom";
 import subscriptionManager from "../app/SubscriptionManager";
-import {ChatBubble, Lock, MoreVert, NotificationsOffOutlined, Public, PublicOff, Send} from "@mui/icons-material";
+import {ChatBubble, Lock, NotificationsOffOutlined, Public, PublicOff, Send} from "@mui/icons-material";
 import Box from "@mui/material/Box";
 import notifier from "../app/Notifier";
 import config from "../app/config";
@@ -37,8 +28,7 @@ import ArticleIcon from '@mui/icons-material/Article';
 import {Trans, useTranslation} from "react-i18next";
 import session from "../app/Session";
 import accountApi from "../app/AccountApi";
-import IconButton from "@mui/material/IconButton";
-import CloseIcon from "@mui/icons-material/Close";
+import CelebrationIcon from '@mui/icons-material/Celebration';
 
 const navWidth = 280;
 
@@ -109,6 +99,7 @@ const NavList = (props) => {
         navigate(routes.account);
     };
 
+    const showUpgradeBanner = config.enable_payments && (!props.account || props.account.plan.upgradeable);
     const showSubscriptionsList = props.subscriptions?.length > 0;
     const showNotificationBrowserNotSupportedBox = !notifier.browserSupported();
     const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
@@ -123,14 +114,14 @@ const NavList = (props) => {
                 {showNotificationContextNotSupportedBox && <NotificationContextNotSupportedAlert/>}
                 {showNotificationGrantBox && <NotificationGrantAlert onRequestPermissionClick={handleRequestNotificationPermission}/>}
                 {!showSubscriptionsList &&
-                    <ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.appRoot}>
+                    <ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.app_root}>
                         <ListItemIcon><ChatBubble/></ListItemIcon>
                         <ListItemText primary={t("nav_button_all_notifications")}/>
                     </ListItemButton>}
                 {showSubscriptionsList &&
                     <>
                         <ListSubheader>{t("nav_topics_title")}</ListSubheader>
-                        <ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.appRoot}>
+                        <ListItemButton onClick={() => navigate(routes.app)} selected={location.pathname === config.app_root}>
                             <ListItemIcon><ChatBubble/></ListItemIcon>
                             <ListItemText primary={t("nav_button_all_notifications")}/>
                         </ListItemButton>
@@ -162,6 +153,34 @@ const NavList = (props) => {
                     <ListItemIcon><AddIcon/></ListItemIcon>
                     <ListItemText primary={t("nav_button_subscribe")}/>
                 </ListItemButton>
+                {showUpgradeBanner &&
+                    <Box sx={{
+                        position: "fixed",
+                        width: `${Navigation.width - 1}px`,
+                        bottom: 0,
+                        mt: 'auto',
+                        background: "linear-gradient(150deg, rgba(196, 228, 221, 0.46) 0%, rgb(255, 255, 255) 100%)",
+                    }}>
+                        <Divider/>
+                        <ListItemButton onClick={() => setSubscribeDialogOpen(true)}>
+                            <ListItemIcon><CelebrationIcon sx={{ color: "#55b86e" }} fontSize="large"/></ListItemIcon>
+                            <ListItemText
+                                sx={{ ml: 1 }}
+                                primary={"Upgrade to ntfy Pro"}
+                                secondary={"Reserve topics, more messages & emails, bigger attachments"}
+                                primaryTypographyProps={{
+                                    style: {
+                                        fontWeight: 500,
+                                        background: "-webkit-linear-gradient(45deg, #09009f, #00ff95 80%)",
+                                        WebkitBackgroundClip: "text",
+                                        WebkitTextFillColor: "transparent"
+                                    }
+                                }}
+                            />
+                        </ListItemButton>
+                    </Box>
+
+                }
             </List>
             <SubscribeDialog
                 key={`subscribeDialog${subscribeDialogKey}`} // Resets dialog when canceled/closed

+ 5 - 2
web/src/components/Preferences.js

@@ -304,7 +304,7 @@ const UserTable = (props) => {
                                    aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell>
                         <TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
                         <TableCell align="right">
-                            {(!session.exists() || user.baseUrl !== config.baseUrl) &&
+                            {(!session.exists() || user.baseUrl !== config.base_url) &&
                                 <>
                                     <IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}>
                                         <EditIcon/>
@@ -314,7 +314,7 @@ const UserTable = (props) => {
                                     </IconButton>
                                 </>
                             }
-                            {session.exists() && user.baseUrl === config.baseUrl &&
+                            {session.exists() && user.baseUrl === config.base_url &&
                                 <Tooltip title={t("prefs_users_table_cannot_delete_or_edit")}>
                                     <span>
                                         <IconButton disabled><EditIcon/></IconButton>
@@ -525,6 +525,9 @@ const Reservations = () => {
                 {limitReached &&
                     <Alert severity="info">
                         You reached your reserved topics limit.
+                        {config.enable_payments &&
+                            <>{" "}<b>Upgrade</b></>
+                        }
                     </Alert>
                 }
             </CardContent>

+ 2 - 2
web/src/components/Signup.js

@@ -43,7 +43,7 @@ const Signup = () => {
             }
         }
     };
-    if (!config.enableSignup) {
+    if (!config.enable_signup) {
         return (
             <AvatarBox>
                 <Typography sx={{ typography: 'h6' }}>{t("signup_disabled")}</Typography>
@@ -114,7 +114,7 @@ const Signup = () => {
                     </Box>
                 }
             </Box>
-            {config.enableLogin &&
+            {config.enable_login &&
                 <Typography sx={{mb: 4}}>
                     <NavLink to={routes.login} variant="body1">
                         {t("signup_already_have_account")}

+ 11 - 38
web/src/components/SubscribeDialog.js

@@ -18,13 +18,8 @@ import {useTranslation} from "react-i18next";
 import session from "../app/Session";
 import routes from "./routes";
 import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi";
-import PublicIcon from '@mui/icons-material/Public';
-import LockIcon from '@mui/icons-material/Lock';
-import PublicOffIcon from '@mui/icons-material/PublicOff';
-import MenuItem from "@mui/material/MenuItem";
-import PopupMenu from "./PopupMenu";
-import ListItemIcon from "@mui/material/ListItemIcon";
 import ReserveTopicSelect from "./ReserveTopicSelect";
+import {useOutletContext} from "react-router-dom";
 
 const publicBaseUrl = "https://ntfy.sh";
 
@@ -36,7 +31,7 @@ const SubscribeDialog = (props) => {
 
     const handleSuccess = async () => {
         console.log(`[SubscribeDialog] Subscribing to topic ${topic}`);
-        const actualBaseUrl = (baseUrl) ? baseUrl : config.baseUrl;
+        const actualBaseUrl = (baseUrl) ? baseUrl : config.base_url;
         const subscription = await subscriptionManager.add(actualBaseUrl, topic);
         if (session.exists()) {
             try {
@@ -81,17 +76,18 @@ const SubscribeDialog = (props) => {
 
 const SubscribePage = (props) => {
     const { t } = useTranslation();
+    const { account } = useOutletContext();
     const [reserveTopicVisible, setReserveTopicVisible] = useState(false);
     const [anotherServerVisible, setAnotherServerVisible] = useState(false);
     const [errorText, setErrorText] = useState("");
-    const [accessAnchorEl, setAccessAnchorEl] = useState(null);
     const [everyone, setEveryone] = useState("deny-all");
-    const baseUrl = (anotherServerVisible) ? props.baseUrl : config.baseUrl;
+    const baseUrl = (anotherServerVisible) ? props.baseUrl : config.base_url;
     const topic = props.topic;
     const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
     const existingBaseUrls = Array
         .from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
-        .filter(s => s !== config.baseUrl);
+        .filter(s => s !== config.base_url);
+    const reserveTopicEnabled = session.exists() && (account?.stats.topics_remaining || 0) > 0;
 
     const handleSubscribe = async () => {
         const user = await userManager.get(baseUrl); // May be undefined
@@ -111,7 +107,7 @@ const SubscribePage = (props) => {
         }
 
         // Reserve topic (if requested)
-        if (session.exists() && baseUrl === config.baseUrl && reserveTopicVisible) {
+        if (session.exists() && baseUrl === config.base_url && reserveTopicVisible) {
             console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`);
             try {
                 await accountApi.upsertAccess(topic, everyone);
@@ -141,7 +137,7 @@ const SubscribePage = (props) => {
             const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
             return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
         } else {
-            const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.baseUrl, topic));
+            const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic));
             return validTopic(topic) && !isExistingTopicUrl;
         }
     })();
@@ -180,30 +176,6 @@ const SubscribePage = (props) => {
                     <Button onClick={() => {props.setTopic(randomAlphanumericString(16))}} style={{flexShrink: "0", marginTop: "0.5em"}}>
                         {t("subscribe_dialog_subscribe_button_generate_topic_name")}
                     </Button>
-                    <PopupMenu
-                        anchorEl={accessAnchorEl}
-                        open={!!accessAnchorEl}
-                        onClose={() => setAccessAnchorEl(null)}
-                    >
-                        <MenuItem onClick={() => setEveryone("private")} selected={everyone === "private"}>
-                            <ListItemIcon>
-                                <LockIcon fontSize="small" />
-                            </ListItemIcon>
-                            Only I can publish and subscribe
-                        </MenuItem>
-                        <MenuItem onClick={() => setEveryone("public-read")} selected={everyone === "public-read"}>
-                            <ListItemIcon>
-                                <PublicOffIcon fontSize="small" />
-                            </ListItemIcon>
-                            I can publish, everyone can subscribe
-                        </MenuItem>
-                        <MenuItem onClick={() => setEveryone("public")} selected={everyone === "public"}>
-                            <ListItemIcon>
-                                <PublicIcon fontSize="small" />
-                            </ListItemIcon>
-                            Everyone can publish and subscribe
-                        </MenuItem>
-                    </PopupMenu>
                 </div>
                 {session.exists() && !anotherServerVisible &&
                     <FormGroup>
@@ -212,6 +184,7 @@ const SubscribePage = (props) => {
                             control={
                                 <Checkbox
                                     fullWidth
+                                    disabled={account.stats.topics_remaining}
                                     checked={reserveTopicVisible}
                                     onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
                                     inputProps={{
@@ -249,7 +222,7 @@ const SubscribePage = (props) => {
                             renderInput={(params) =>
                                 <TextField
                                     {...params}
-                                    placeholder={config.baseUrl}
+                                    placeholder={config.base_url}
                                     variant="standard"
                                     aria-label={t("subscribe_dialog_subscribe_base_url_label")}
                                 />
@@ -271,7 +244,7 @@ const LoginPage = (props) => {
     const [username, setUsername] = useState("");
     const [password, setPassword] = useState("");
     const [errorText, setErrorText] = useState("");
-    const baseUrl = (props.baseUrl) ? props.baseUrl : config.baseUrl;
+    const baseUrl = (props.baseUrl) ? props.baseUrl : config.base_url;
     const topic = props.topic;
     const handleLogin = async () => {
         const user = {baseUrl, username, password};

+ 1 - 1
web/src/components/hooks.js

@@ -60,7 +60,7 @@ export const useAutoSubscribe = (subscriptions, selected) => {
         setHasRun(true);
         const eligible = params.topic && !selected && !disallowedTopic(params.topic);
         if (eligible) {
-            const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : config.baseUrl;
+            const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : config.base_url;
             console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
             (async () => {
                 const subscription = await subscriptionManager.add(baseUrl, params.topic);

+ 2 - 2
web/src/components/routes.js

@@ -9,13 +9,13 @@ const routes = {
     login: "/login",
     signup: "/signup",
     resetPassword: "/reset-password",
-    app: config.appRoot,
+    app: config.app_root,
     account: "/account",
     settings: "/settings",
     subscription: "/:topic",
     subscriptionExternal: "/:baseUrl/:topic",
     forSubscription: (subscription) => {
-        if (subscription.baseUrl !== config.baseUrl) {
+        if (subscription.baseUrl !== config.base_url) {
             return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`;
         }
         return `/${subscription.topic}`;