Tiers make sense for admins now

This commit is contained in:
binwiederhier 2023-01-09 15:40:46 -05:00
parent d8032e1c9e
commit 3aba7404fc
18 changed files with 457 additions and 225 deletions

11
web/package-lock.json generated
View file

@ -14,6 +14,7 @@
"@mui/material": "latest",
"dexie": "^3.2.1",
"dexie-react-hooks": "^1.1.1",
"humanize-duration": "^3.27.3",
"i18next": "^21.6.14",
"i18next-browser-languagedetector": "^6.1.4",
"i18next-http-backend": "^1.4.0",
@ -8837,6 +8838,11 @@
"node": ">=10.17.0"
}
},
"node_modules/humanize-duration": {
"version": "3.27.3",
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.3.tgz",
"integrity": "sha512-iimHkHPfIAQ8zCDQLgn08pRqSVioyWvnGfaQ8gond2wf7Jq2jJ+24ykmnRyiz3fIldcn4oUuQXpjqKLhSVR7lw=="
},
"node_modules/i18next": {
"version": "21.10.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.10.0.tgz",
@ -23381,6 +23387,11 @@
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
},
"humanize-duration": {
"version": "3.27.3",
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.3.tgz",
"integrity": "sha512-iimHkHPfIAQ8zCDQLgn08pRqSVioyWvnGfaQ8gond2wf7Jq2jJ+24ykmnRyiz3fIldcn4oUuQXpjqKLhSVR7lw=="
},
"i18next": {
"version": "21.10.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.10.0.tgz",

View file

@ -15,6 +15,7 @@
"@mui/material": "latest",
"dexie": "^3.2.1",
"dexie-react-hooks": "^1.1.1",
"humanize-duration": "^3.27.3",
"i18next": "^21.6.14",
"i18next-browser-languagedetector": "^6.1.4",
"i18next-http-backend": "^1.4.0",

View file

@ -179,17 +179,15 @@
"account_usage_unlimited": "Unlimited",
"account_usage_limits_reset_daily": "Usage limits are reset daily at midnight (UTC)",
"account_usage_tier_title": "Account type",
"account_usage_tier_code_default": "Default",
"account_usage_tier_code_unlimited": "Unlimited",
"account_usage_tier_code_none": "None",
"account_usage_tier_code_pro": "Pro",
"account_usage_tier_code_business": "Business",
"account_usage_tier_code_business_plus": "Business Plus",
"account_usage_tier_admin": "Admin",
"account_usage_tier_none": "Basic",
"account_usage_tier_upgrade_button": "Upgrade to Pro",
"account_usage_tier_change_button": "Change",
"account_usage_messages_title": "Published messages",
"account_usage_emails_title": "Emails sent",
"account_usage_topics_title": "Reserved topics",
"account_usage_reservations_title": "Reserved topics",
"account_usage_attachment_storage_title": "Attachment storage",
"account_usage_attachment_storage_subtitle": "{{filesize}} per file",
"account_usage_attachment_storage_description": "{{filesize}} per file, deleted after {{expiry}}",
"account_usage_basis_ip_description": "Usage stats and limits for this account are based on your IP address, so they may be shared with other users. Limits shown above are approximates based on the existing rate limits.",
"account_delete_title": "Delete account",
"account_delete_description": "Permanently delete your account",

View file

@ -24,6 +24,10 @@ import accountApi, {UnauthorizedError} from "../app/AccountApi";
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import {Pref, PrefGroup} from "./Pref";
import db from "../app/db";
import i18n from "i18next";
import humanizeDuration from "humanize-duration";
import UpgradeDialog from "./UpgradeDialog";
import CelebrationIcon from "@mui/icons-material/Celebration";
const Account = () => {
if (!session.exists()) {
@ -166,10 +170,12 @@ const ChangePasswordDialog = (props) => {
const Stats = () => {
const { t } = useTranslation();
const { account } = useOutletContext();
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
if (!account) {
return <></>;
}
const tierCode = account.tier.code ?? "none";
const normalize = (value, max) => Math.min(value / max * 100, 100);
const barColor = (remaining, limit) => {
if (account.role === "admin") {
@ -188,34 +194,63 @@ const Stats = () => {
<PrefGroup>
<Pref title={t("account_usage_tier_title")}>
<div>
{account.role === "admin"
? <>{t("account_usage_unlimited")} <Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
: t(`account_usage_tier_code_${tierCode}`)}
{config.enable_payments && account.tier.upgradeable &&
<em>{" "}
<Link onClick={() => {}}>Upgrade</Link>
</em>
{account.role === "admin" &&
<>
{t("account_usage_tier_admin")}
{" "}{account.tier ? `(with ${account.tier.name} tier)` : `(no tier)`}
</>
}
{account.role === "user" && account.tier &&
<>{account.tier.name}</>
}
{account.role === "user" && !account.tier &&
t("account_usage_tier_none")
}
{config.enable_payments && account.role === "user" && (!account.tier || !account.tier.paid) &&
<Button
variant="outlined"
size="small"
startIcon={<CelebrationIcon sx={{ color: "#55b86e" }}/>}
onClick={() => setUpgradeDialogOpen(true)}
sx={{ml: 1}}
>{t("account_usage_tier_upgrade_button")}</Button>
}
{config.enable_payments && account.role === "user" && account.tier?.paid &&
<Button
variant="outlined"
size="small"
onClick={() => setUpgradeDialogOpen(true)}
sx={{ml: 1}}
>{t("account_usage_tier_change_button")}</Button>
}
<UpgradeDialog
open={upgradeDialogOpen}
onCancel={() => setUpgradeDialogOpen(false)}
/>
</div>
</Pref>
<Pref title={t("account_usage_topics_title")}>
{account.limits.reservations > 0 &&
<>
<div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.reservations}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.reservations }) : t("account_usage_unlimited")}</Typography>
</div>
<LinearProgress
variant="determinate"
value={account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100}
color={barColor(account.stats.reservations_remaining, account.limits.reservations)}
/>
</>
}
{account.limits.reservations === 0 &&
<em>No reserved topics for this account</em>
}
</Pref>
{account.role !== "admin" &&
<Pref title={t("account_usage_reservations_title")}>
{account.limits.reservations > 0 &&
<>
<div>
<Typography variant="body2"
sx={{float: "left"}}>{account.stats.reservations}</Typography>
<Typography variant="body2"
sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", {limit: account.limits.reservations}) : t("account_usage_unlimited")}</Typography>
</div>
<LinearProgress
variant="determinate"
value={account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100}
color={barColor(account.stats.reservations_remaining, account.limits.reservations)}
/>
</>
}
{account.limits.reservations === 0 &&
<em>No reserved topics for this account</em>
}
</Pref>
}
<Pref title={
<>
{t("account_usage_messages_title")}
@ -224,11 +259,11 @@ const Stats = () => {
}>
<div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
</div>
<LinearProgress
variant="determinate"
value={account.limits.messages > 0 ? 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>
@ -248,14 +283,17 @@ const Stats = () => {
color={account?.role !== "admin" && account.stats.emails_remaining === 0 ? 'error' : 'primary'}
/>
</Pref>
<Pref title={
<>
{t("account_usage_attachment_storage_title")}
{account.role === "user" &&
<Tooltip title={t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) })}><span><InfoIcon/></span></Tooltip>
}
</>
}>
<Pref
alignTop
title={t("account_usage_attachment_storage_title")}
description={t("account_usage_attachment_storage_description", {
filesize: formatBytes(account.limits.attachment_file_size),
expiry: humanizeDuration(account.limits.attachment_expiry_duration * 1000, {
language: i18n.language,
fallbacks: ["en"]
})
})}
>
<div>
<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>
@ -269,7 +307,7 @@ const Stats = () => {
</PrefGroup>
{account.limits.basis === "ip" &&
<Typography variant="body1">
<em>{t("account_usage_basis_ip_description")}</em>
{t("account_usage_basis_ip_description")}
</Typography>
}
</Card>

View file

@ -29,6 +29,7 @@ import {Trans, useTranslation} from "react-i18next";
import session from "../app/Session";
import accountApi from "../app/AccountApi";
import CelebrationIcon from '@mui/icons-material/Celebration';
import UpgradeDialog from "./UpgradeDialog";
const navWidth = 280;
@ -99,7 +100,9 @@ const NavList = (props) => {
navigate(routes.account);
};
const showUpgradeBanner = config.enable_payments && (!props.account || props.account.tier.upgradeable);
const isAdmin = props.account?.role === "admin";
const isPaid = props.account?.tier?.paid;
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 showNotificationBrowserNotSupportedBox = !notifier.browserSupported();
const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
@ -154,32 +157,7 @@ const NavList = (props) => {
<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>
<UpgradeBanner/>
}
</List>
<SubscribeDialog
@ -193,6 +171,41 @@ const NavList = (props) => {
);
};
const UpgradeBanner = () => {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<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={() => setDialogOpen(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>
<UpgradeDialog
open={dialogOpen}
onCancel={() => setDialogOpen(false)}
/>
</Box>
);
};
const SubscriptionList = (props) => {
const sortedSubscriptions = props.subscriptions.sort( (a, b) => {
return (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic)) ? -1 : 1;

View file

@ -9,6 +9,7 @@ export const PrefGroup = (props) => {
};
export const Pref = (props) => {
const justifyContent = (props.alignTop) ? "normal" : "center";
return (
<div
role="row"
@ -27,7 +28,7 @@ export const Pref = (props) => {
flex: '1 0 40%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
justifyContent: justifyContent,
paddingRight: '30px'
}}
>
@ -40,7 +41,7 @@ export const Pref = (props) => {
flex: '1 0 calc(60% - 50px)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
justifyContent: justifyContent
}}
>
{props.children}

View file

@ -0,0 +1,44 @@
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 DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import {Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery} from "@mui/material";
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 {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 fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSuccess = async () => {
// TODO
}
return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>Upgrade to Pro</DialogTitle>
<DialogContent>
Content
</DialogContent>
<DialogFooter>
Footer
</DialogFooter>
</Dialog>
);
};
export default UpgradeDialog;