Upgrade banner
parent
a91da7cf2c
commit
3280c2c440
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -81,17 +81,17 @@ 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,
|
||||
Upgradeable: v.user.Plan.Upgradeable,
|
||||
}
|
||||
} else if v.user.Role == user.RoleAdmin {
|
||||
response.Plan = &apiAccountPlan{
|
||||
Code: string(user.PlanUnlimited),
|
||||
Upgradable: false,
|
||||
Upgradeable: false,
|
||||
}
|
||||
} else {
|
||||
response.Plan = &apiAccountPlan{
|
||||
Code: string(user.PlanDefault),
|
||||
Upgradable: true,
|
||||
Upgradeable: true,
|
||||
}
|
||||
}
|
||||
reservations, err := s.userManager.Reservations(v.user.Name)
|
||||
|
@ -112,7 +112,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
|
|||
response.Role = string(user.RoleAnonymous)
|
||||
response.Plan = &apiAccountPlan{
|
||||
Code: string(user.PlanNone),
|
||||
Upgradable: true,
|
||||
Upgradeable: true,
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
|
@ -236,7 +236,7 @@ type apiAccountTokenResponse struct {
|
|||
|
||||
type apiAccountPlan struct {
|
||||
Code string `json:"code"`
|
||||
Upgradable bool `json:"upgradable"`
|
||||
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"`
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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"]
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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"!
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`;
|
||||
|
|
Loading…
Reference in New Issue