Make manual eslint fixes

These are safe fixes, more complicated fixes can be done separately
(just disabled those errors for now).

- Reorder declarations to fix `no-use-before-define`
- Rename parameters for `no-shadow`
- Remove unused parameters, functions, imports
- Switch from `++` and `—` to `+= 1` and `-= 1` for `no-unary`
- Use object spreading instead of parameter reassignment in auth utils
- Use `window.location` instead of `location` global
- Use inline JSX strings instead of unescaped values
-
This commit is contained in:
nimbleghost 2023-05-24 10:20:15 +02:00
parent 8319f1cf26
commit 59011c8a32
20 changed files with 369 additions and 351 deletions

View file

@ -439,23 +439,6 @@ const AddPhoneNumberDialog = (props) => {
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
const handleDialogSubmit = async () => {
if (!verificationCodeSent) {
await verifyPhone();
} else {
await checkVerifyPhone();
}
};
const handleCancel = () => {
if (verificationCodeSent) {
setVerificationCodeSent(false);
setCode("");
} else {
props.onClose();
}
};
const verifyPhone = async () => {
try {
setSending(true);
@ -490,6 +473,23 @@ const AddPhoneNumberDialog = (props) => {
}
};
const handleDialogSubmit = async () => {
if (!verificationCodeSent) {
await verifyPhone();
} else {
await checkVerifyPhone();
}
};
const handleCancel = () => {
if (verificationCodeSent) {
setVerificationCodeSent(false);
setCode("");
} else {
props.onClose();
}
};
return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>{t("account_basics_phone_numbers_dialog_title")}</DialogTitle>
@ -771,10 +771,6 @@ const Tokens = () => {
setDialogOpen(false);
};
const handleDialogSubmit = async (user) => {
setDialogOpen(false);
//
};
return (
<Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}>
<CardContent sx={{ paddingBottom: 1 }}>
@ -998,7 +994,6 @@ const TokenDialog = (props) => {
const TokenDeleteDialog = (props) => {
const { t } = useTranslation();
const [error, setError] = useState("");
const handleSubmit = async () => {
try {
@ -1008,8 +1003,6 @@ const TokenDeleteDialog = (props) => {
console.log(`[Account] Error deleting token`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
}
};

View file

@ -1,5 +1,5 @@
import * as React from "react";
import { createContext, Suspense, useContext, useEffect, useState } from "react";
import { createContext, Suspense, useContext, useEffect, useState, useMemo } from "react";
import Box from "@mui/material/Box";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
@ -30,11 +30,14 @@ export const AccountContext = createContext(null);
const App = () => {
const [account, setAccount] = useState(null);
const contextValue = useMemo(() => ({ account, setAccount }), [account, setAccount]);
return (
<Suspense fallback={<Loader />}>
<BrowserRouter>
<ThemeProvider theme={theme}>
<AccountContext.Provider value={{ account, setAccount }}>
<AccountContext.Provider value={contextValue}>
<CssBaseline />
<ErrorBoundary>
<Routes>
@ -56,6 +59,10 @@ const App = () => {
);
};
const updateTitle = (newNotificationsCount) => {
document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
};
const Layout = () => {
const params = useParams();
const { account, setAccount } = useContext(AccountContext);
@ -115,7 +122,7 @@ const Main = (props) => (
width: { sm: `calc(100% - ${Navigation.width}px)` },
height: "100vh",
overflow: "auto",
backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]),
backgroundColor: ({ palette }) => (palette.mode === "light" ? palette.grey[100] : palette.grey[900]),
}}
>
{props.children}
@ -127,15 +134,11 @@ const Loader = () => (
open
sx={{
zIndex: 100000,
backgroundColor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]),
backgroundColor: ({ palette }) => (palette.mode === "light" ? palette.grey[100] : palette.grey[900]),
}}
>
<CircularProgress color="success" disableShrink />
</Backdrop>
);
const updateTitle = (newNotificationsCount) => {
document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
};
export default App;

View file

@ -79,8 +79,6 @@ const EmojiPicker = (props) => {
inputProps={{
role: "searchbox",
"aria-label": t("emoji_picker_search_placeholder"),
}}
InputProps={{
endAdornment: (
<InputAdornment position="end" sx={{ display: search ? "" : "none" }}>
<IconButton size="small" onClick={handleSearchClear} edge="end" aria-label={t("emoji_picker_search_clear")}>
@ -132,6 +130,18 @@ const Category = (props) => {
);
};
const emojiMatches = (emoji, words) => {
if (words.length === 0) {
return true;
}
for (const word of words) {
if (emoji.searchBase.indexOf(word) === -1) {
return false;
}
}
return true;
};
const Emoji = (props) => {
const { emoji } = props;
const matches = emojiMatches(emoji, props.search);
@ -158,16 +168,4 @@ const EmojiDiv = styled("div")({
},
});
const emojiMatches = (emoji, words) => {
if (words.length === 0) {
return true;
}
for (const word of words) {
if (emoji.searchBase.indexOf(word) === -1) {
return false;
}
}
return true;
};
export default EmojiPicker;

View file

@ -69,16 +69,6 @@ class ErrorBoundaryImpl extends React.Component {
navigator.clipboard.writeText(stack);
}
render() {
if (this.state.error) {
if (this.state.unsupportedIndexedDB) {
return this.renderUnsupportedIndexedDB();
}
return this.renderError();
}
return this.props.children;
}
renderUnsupportedIndexedDB() {
const { t } = this.props;
return (
@ -130,6 +120,16 @@ class ErrorBoundaryImpl extends React.Component {
</div>
);
}
render() {
if (this.state.error) {
if (this.state.unsupportedIndexedDB) {
return this.renderUnsupportedIndexedDB();
}
return this.renderError();
}
return this.props.children;
}
}
const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); // Adds props.t

View file

@ -85,6 +85,10 @@ const NavList = (props) => {
setSubscribeDialogKey((prev) => prev + 1);
};
const handleRequestNotificationPermission = () => {
notifier.maybeRequestPermission((granted) => props.onNotificationGranted(granted));
};
const handleSubscribeSubmit = (subscription) => {
console.log(`[Navigation] New subscription: ${subscription.id}`, subscription);
handleSubscribeReset();
@ -92,10 +96,6 @@ const NavList = (props) => {
handleRequestNotificationPermission();
};
const handleRequestNotificationPermission = () => {
notifier.maybeRequestPermission((granted) => props.onNotificationGranted(granted));
};
const handleAccountClick = () => {
accountApi.sync(); // Dangle!
navigate(routes.account);

View file

@ -34,6 +34,13 @@ import logoOutline from "../img/ntfy-outline.svg";
import AttachmentIcon from "./AttachmentIcon";
import { useAutoSubscribe } from "./hooks";
const priorityFiles = {
1: priority1,
2: priority2,
4: priority4,
5: priority5,
};
export const AllSubscriptions = () => {
const { subscriptions } = useOutletContext();
if (!subscriptions) {
@ -131,6 +138,25 @@ const NotificationList = (props) => {
);
};
/**
* Replace links with <Link/> components; this is a combination of the genius function
* in [1] and the regex in [2].
*
* [1] https://github.com/facebook/react/issues/3386#issuecomment-78605760
* [2] https://github.com/bryanwoods/autolink-js/blob/master/autolink.js#L9
*/
const autolink = (s) => {
const parts = s.split(/(\bhttps?:\/\/[-A-Z0-9+\u0026\u2019@#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026@#/%=~()_|]\b)/gi);
for (let i = 1; i < parts.length; i += 2) {
parts[i] = (
<Link key={i} href={parts[i]} underline="hover" target="_blank" rel="noreferrer,noopener">
{shortUrl(parts[i])}
</Link>
);
}
return <>{parts}</>;
};
const NotificationItem = (props) => {
const { t } = useTranslation();
const { notification } = props;
@ -248,32 +274,6 @@ const NotificationItem = (props) => {
);
};
/**
* Replace links with <Link/> components; this is a combination of the genius function
* in [1] and the regex in [2].
*
* [1] https://github.com/facebook/react/issues/3386#issuecomment-78605760
* [2] https://github.com/bryanwoods/autolink-js/blob/master/autolink.js#L9
*/
const autolink = (s) => {
const parts = s.split(/(\bhttps?:\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|]\b)/gi);
for (let i = 1; i < parts.length; i += 2) {
parts[i] = (
<Link key={i} href={parts[i]} underline="hover" target="_blank" rel="noreferrer,noopener">
{shortUrl(parts[i])}
</Link>
);
}
return <>{parts}</>;
};
const priorityFiles = {
1: priority1,
2: priority2,
4: priority4,
5: priority5,
};
const Attachment = (props) => {
const { t } = useTranslation();
const { attachment } = props;
@ -414,6 +414,52 @@ const UserActions = (props) => (
</>
);
const ACTION_PROGRESS_ONGOING = 1;
const ACTION_PROGRESS_SUCCESS = 2;
const ACTION_PROGRESS_FAILED = 3;
const ACTION_LABEL_SUFFIX = {
[ACTION_PROGRESS_ONGOING]: " …",
[ACTION_PROGRESS_SUCCESS]: " ✔",
[ACTION_PROGRESS_FAILED]: " ❌",
};
const updateActionStatus = (notification, action, progress, error) => {
// TODO(eslint): Fix by spreading? Does the code depend on the change, though?
// eslint-disable-next-line no-param-reassign
notification.actions = notification.actions.map((a) => {
if (a.id !== action.id) {
return a;
}
return { ...a, progress, error };
});
subscriptionManager.updateNotification(notification);
};
const performHttpAction = async (notification, action) => {
console.log(`[Notifications] Performing HTTP user action`, action);
try {
updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null);
const response = await fetch(action.url, {
method: action.method ?? "POST",
headers: action.headers ?? {},
// This must not null-coalesce to a non nullish value. Otherwise, the fetch API
// will reject it for "having a body"
body: action.body,
});
console.log(`[Notifications] HTTP user action response`, response);
const success = response.status >= 200 && response.status <= 299;
if (success) {
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
} else {
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
}
} catch (e) {
console.log(`[Notifications] HTTP action failed`, e);
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`);
}
};
const UserAction = (props) => {
const { t } = useTranslation();
const { notification } = props;
@ -468,53 +514,9 @@ const UserAction = (props) => {
return null; // Others
};
const performHttpAction = async (notification, action) => {
console.log(`[Notifications] Performing HTTP user action`, action);
try {
updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null);
const response = await fetch(action.url, {
method: action.method ?? "POST",
headers: action.headers ?? {},
// This must not null-coalesce to a non nullish value. Otherwise, the fetch API
// will reject it for "having a body"
body: action.body,
});
console.log(`[Notifications] HTTP user action response`, response);
const success = response.status >= 200 && response.status <= 299;
if (success) {
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
} else {
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
}
} catch (e) {
console.log(`[Notifications] HTTP action failed`, e);
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`);
}
};
const updateActionStatus = (notification, action, progress, error) => {
notification.actions = notification.actions.map((a) => {
if (a.id !== action.id) {
return a;
}
return { ...a, progress, error };
});
subscriptionManager.updateNotification(notification);
};
const ACTION_PROGRESS_ONGOING = 1;
const ACTION_PROGRESS_SUCCESS = 2;
const ACTION_PROGRESS_FAILED = 3;
const ACTION_LABEL_SUFFIX = {
[ACTION_PROGRESS_ONGOING]: " …",
[ACTION_PROGRESS_SUCCESS]: " ✔",
[ACTION_PROGRESS_FAILED]: " ❌",
};
const NoNotifications = (props) => {
const { t } = useTranslation();
const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
const topicShortUrlResolved = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
return (
<VerticallyCenteredContainer maxWidth="xs">
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
@ -525,7 +527,10 @@ const NoNotifications = (props) => {
<Paragraph>{t("notifications_none_for_topic_description")}</Paragraph>
<Paragraph>
{t("notifications_example")}:<br />
<tt>$ curl -d "Hi" {shortUrl}</tt>
<tt>
{'$ curl -d "Hi" '}
{topicShortUrlResolved}
</tt>
</Paragraph>
<Paragraph>
<ForMoreDetails />
@ -537,7 +542,7 @@ const NoNotifications = (props) => {
const NoNotificationsWithoutSubscription = (props) => {
const { t } = useTranslation();
const subscription = props.subscriptions[0];
const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic);
const topicShortUrlResolved = topicShortUrl(subscription.baseUrl, subscription.topic);
return (
<VerticallyCenteredContainer maxWidth="xs">
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
@ -548,7 +553,10 @@ const NoNotificationsWithoutSubscription = (props) => {
<Paragraph>{t("notifications_none_for_any_description")}</Paragraph>
<Paragraph>
{t("notifications_example")}:<br />
<tt>$ curl -d "Hi" {shortUrl}</tt>
<tt>
{'$ curl -d "Hi" '}
{topicShortUrlResolved}
</tt>
</Paragraph>
<Paragraph>
<ForMoreDetails />

View file

@ -47,9 +47,22 @@ import prefs from "../app/Prefs";
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs";
import { UnauthorizedError } from "../app/errors";
import subscriptionManager from "../app/SubscriptionManager";
import { subscribeTopic } from "./SubscribeDialog";
const maybeUpdateAccountSettings = async (payload) => {
if (!session.exists()) {
return;
}
try {
await accountApi.updateSettings(payload);
} catch (e) {
console.log(`[Preferences] Error updating account settings`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}
};
const Preferences = () => (
<Container maxWidth="md" sx={{ marginTop: 3, marginBottom: 3 }}>
<Stack spacing={3}>
@ -181,10 +194,12 @@ const DeleteAfter = () => {
},
});
};
if (deleteAfter === null || deleteAfter === undefined) {
// !deleteAfter will not work with "0"
return null; // While loading
}
const description = (() => {
switch (deleteAfter) {
case 0:
@ -197,8 +212,11 @@ const DeleteAfter = () => {
return t("prefs_notifications_delete_after_one_week_description");
case 2592000:
return t("prefs_notifications_delete_after_one_month_description");
default:
return "";
}
})();
return (
<Pref labelId={labelId} title={t("prefs_notifications_delete_after_title")} description={description}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
@ -674,18 +692,4 @@ const ReservationsTable = (props) => {
);
};
const maybeUpdateAccountSettings = async (payload) => {
if (!session.exists()) {
return;
}
try {
await accountApi.updateSettings(payload);
} catch (e) {
console.log(`[Preferences] Error updating account settings`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}
};
export default Preferences;

View file

@ -171,34 +171,33 @@ const PublishDialog = (props) => {
const checkAttachmentLimits = async (file) => {
try {
const account = await accountApi.get();
const fileSizeLimit = account.limits.attachment_file_size ?? 0;
const remainingBytes = account.stats.attachment_total_size_remaining;
const apiAccount = await accountApi.get();
const fileSizeLimit = apiAccount.limits.attachment_file_size ?? 0;
const remainingBytes = apiAccount.stats.attachment_total_size_remaining;
const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit;
const quotaReached = remainingBytes > 0 && file.size > remainingBytes;
if (fileSizeLimitReached && quotaReached) {
return setAttachFileError(
setAttachFileError(
t("publish_dialog_attachment_limits_file_and_quota_reached", {
fileSizeLimit: formatBytes(fileSizeLimit),
remainingBytes: formatBytes(remainingBytes),
})
);
}
if (fileSizeLimitReached) {
return setAttachFileError(
} else if (fileSizeLimitReached) {
setAttachFileError(
t("publish_dialog_attachment_limits_file_reached", {
fileSizeLimit: formatBytes(fileSizeLimit),
})
);
}
if (quotaReached) {
return setAttachFileError(
} else if (quotaReached) {
setAttachFileError(
t("publish_dialog_attachment_limits_quota_reached", {
remainingBytes: formatBytes(remainingBytes),
})
);
} else {
setAttachFileError("");
}
setAttachFileError("");
} catch (e) {
console.log(`[PublishDialog] Retrieving attachment limits failed`, e);
if (e instanceof UnauthorizedError) {
@ -213,6 +212,13 @@ const PublishDialog = (props) => {
attachFileInput.current.click();
};
const updateAttachFile = async (file) => {
setAttachFile(file);
setFilename(file.name);
props.onResetOpenMode();
await checkAttachmentLimits(file);
};
const handleAttachFileChanged = async (ev) => {
await updateAttachFile(ev.target.files[0]);
};
@ -223,13 +229,6 @@ const PublishDialog = (props) => {
await updateAttachFile(ev.dataTransfer.files[0]);
};
const updateAttachFile = async (file) => {
setAttachFile(file);
setFilename(file.name);
props.onResetOpenMode();
await checkAttachmentLimits(file);
};
const handleAttachFileDragLeave = () => {
setDropZone(false);
if (props.openMode === PublishDialog.OPEN_MODE_DRAG) {
@ -242,7 +241,7 @@ const PublishDialog = (props) => {
};
const handleEmojiPick = (emoji) => {
setTags((tags) => (tags.trim() ? `${tags.trim()}, ${emoji}` : emoji));
setTags((prevTags) => (prevTags.trim() ? `${prevTags.trim()}, ${emoji}` : emoji));
};
const handleEmojiClose = () => {
@ -374,23 +373,23 @@ const PublishDialog = (props) => {
"aria-label": t("publish_dialog_priority_label"),
}}
>
{[5, 4, 3, 2, 1].map((priority) => (
{[5, 4, 3, 2, 1].map((priorityMenuItem) => (
<MenuItem
key={`priorityMenuItem${priority}`}
value={priority}
key={`priorityMenuItem${priorityMenuItem}`}
value={priorityMenuItem}
aria-label={t("notifications_priority_x", {
priority,
priority: priorityMenuItem,
})}
>
<div style={{ display: "flex", alignItems: "center" }}>
<img
src={priorities[priority].file}
src={priorities[priorityMenuItem].file}
style={{ marginRight: "8px" }}
alt={t("notifications_priority_x", {
priority,
priority: priorityMenuItem,
})}
/>
<div>{priorities[priority].label}</div>
<div>{priorities[priorityMenuItem].label}</div>
</div>
</MenuItem>
))}
@ -469,6 +468,8 @@ const PublishDialog = (props) => {
}}
>
{account?.phone_numbers?.map((phoneNumber, i) => (
// TODO(eslint): Possibly just use the phone number as a key?
// eslint-disable-next-line react/no-array-index-key
<MenuItem key={`phoneNumberMenuItem${i}`} value={phoneNumber} aria-label={phoneNumber}>
{t("publish_dialog_call_item", { number: phoneNumber })}
</MenuItem>
@ -716,7 +717,7 @@ const Row = (props) => (
);
const ClosableRow = (props) => {
const closable = props.hasOwnProperty("closable") ? props.closable : true;
const closable = props.closable !== undefined ? props.closable : true;
return (
<Row>
{props.children}
@ -823,10 +824,7 @@ const ExpandingTextField = (props) => {
variant="standard"
sx={{ width: `${textWidth}px`, borderBottom: "none" }}
InputProps={{
style: { fontSize: theme.typography[props.variant].fontSize },
}}
inputProps={{
style: { paddingBottom: 0, paddingTop: 0 },
style: { fontSize: theme.typography[props.variant].fontSize, paddingBottom: 0, paddingTop: 0 },
"aria-label": props.placeholder,
}}
disabled={props.disabled}
@ -840,6 +838,7 @@ const DropArea = (props) => {
// This is where we could disallow certain files to be dragged in.
// For now we allow all files.
// eslint-disable-next-line no-param-reassign
ev.dataTransfer.dropEffect = "copy";
ev.preventDefault();
};

View file

@ -25,6 +25,21 @@ import { ReserveLimitChip } from "./SubscriptionPopup";
const publicBaseUrl = "https://ntfy.sh";
export const subscribeTopic = async (baseUrl, topic) => {
const subscription = await subscriptionManager.add(baseUrl, topic);
if (session.exists()) {
try {
await accountApi.addSubscription(baseUrl, topic);
} catch (e) {
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}
}
return subscription;
};
const SubscribeDialog = (props) => {
const [baseUrl, setBaseUrl] = useState("");
const [topic, setTopic] = useState("");
@ -296,19 +311,4 @@ const LoginPage = (props) => {
);
};
export const subscribeTopic = async (baseUrl, topic) => {
const subscription = await subscriptionManager.add(baseUrl, topic);
if (session.exists()) {
try {
await accountApi.addSubscription(baseUrl, topic);
} catch (e) {
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}
}
return subscription;
};
export default SubscribeDialog;

View file

@ -241,8 +241,6 @@ const DisplayNameDialog = (props) => {
inputProps={{
maxLength: 64,
"aria-label": t("display_name_dialog_placeholder"),
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setDisplayName("")} edge="end">
@ -292,20 +290,17 @@ const LimitReachedChip = () => {
);
};
export const ProChip = () => {
const { t } = useTranslation();
return (
<Chip
label="ntfy Pro"
variant="outlined"
color="primary"
sx={{
opacity: 0.8,
fontWeight: "bold",
borderWidth: "2px",
height: "24px",
marginLeft: "5px",
}}
/>
);
};
export const ProChip = () => (
<Chip
label="ntfy Pro"
variant="outlined"
color="primary"
sx={{
opacity: 0.8,
fontWeight: "bold",
borderWidth: "2px",
height: "24px",
marginLeft: "5px",
}}
/>
);

View file

@ -24,6 +24,33 @@ import session from "../app/Session";
import accountApi, { SubscriptionInterval } from "../app/AccountApi";
import theme from "./theme";
const Feature = (props) => <FeatureItem feature>{props.children}</FeatureItem>;
const NoFeature = (props) => <FeatureItem feature={false}>{props.children}</FeatureItem>;
const FeatureItem = (props) => (
<ListItem disableGutters sx={{ m: 0, p: 0 }}>
<ListItemIcon sx={{ minWidth: "24px" }}>
{props.feature && <Check fontSize="small" sx={{ color: "#338574" }} />}
{!props.feature && <Close fontSize="small" sx={{ color: "gray" }} />}
</ListItemIcon>
<ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} />
</ListItem>
);
const Action = {
REDIRECT_SIGNUP: 1,
CREATE_SUBSCRIPTION: 2,
UPDATE_SUBSCRIPTION: 3,
CANCEL_SUBSCRIPTION: 4,
};
const Banner = {
CANCEL_WARNING: 1,
PRORATION_INFO: 2,
RESERVATIONS_WARNING: 3,
};
const UpgradeDialog = (props) => {
const { t } = useTranslation();
const { account } = useContext(AccountContext); // May be undefined!
@ -120,12 +147,12 @@ const UpgradeDialog = (props) => {
discount = Math.round(((newTier.prices.month * 12) / newTier.prices.year - 1) * 100);
} else {
let n = 0;
for (const t of tiers) {
if (t.prices) {
const tierDiscount = Math.round(((t.prices.month * 12) / t.prices.year - 1) * 100);
for (const tier of tiers) {
if (tier.prices) {
const tierDiscount = Math.round(((tier.prices.month * 12) / tier.prices.year - 1) * 100);
if (tierDiscount > discount) {
discount = tierDiscount;
n++;
n += 1;
}
}
}
@ -210,7 +237,7 @@ const UpgradeDialog = (props) => {
<Alert severity="warning" sx={{ fontSize: "1rem" }}>
<Trans
i18nKey="account_upgrade_dialog_reservations_warning"
count={account?.reservations.length - newTier?.limits.reservations}
count={(account?.reservations.length ?? 0) - (newTier?.limits.reservations ?? 0)}
components={{
Link: <NavLink to={routes.settings} />,
}}
@ -396,31 +423,4 @@ const TierCard = (props) => {
);
};
const Feature = (props) => <FeatureItem feature>{props.children}</FeatureItem>;
const NoFeature = (props) => <FeatureItem feature={false}>{props.children}</FeatureItem>;
const FeatureItem = (props) => (
<ListItem disableGutters sx={{ m: 0, p: 0 }}>
<ListItemIcon sx={{ minWidth: "24px" }}>
{props.feature && <Check fontSize="small" sx={{ color: "#338574" }} />}
{!props.feature && <Close fontSize="small" sx={{ color: "gray" }} />}
</ListItemIcon>
<ListItemText sx={{ mt: "2px", mb: "2px" }} primary={<Typography variant="body1">{props.children}</Typography>} />
</ListItem>
);
const Action = {
REDIRECT_SIGNUP: 1,
CREATE_SUBSCRIPTION: 2,
UPDATE_SUBSCRIPTION: 3,
CANCEL_SUBSCRIPTION: 4,
};
const Banner = {
CANCEL_WARNING: 1,
PRORATION_INFO: 2,
RESERVATIONS_WARNING: 3,
};
export default UpgradeDialog;

View file

@ -22,15 +22,6 @@ export const useConnectionListeners = (account, subscriptions, users) => {
// Register listeners for incoming messages, and connection state changes
useEffect(
() => {
const handleMessage = async (subscriptionId, message) => {
const subscription = await subscriptionManager.get(subscriptionId);
if (subscription.internal) {
await handleInternalMessage(message);
} else {
await handleNotification(subscriptionId, message);
}
};
const handleInternalMessage = async (message) => {
console.log(`[ConnectionListener] Received message on sync topic`, message.message);
try {
@ -53,8 +44,19 @@ export const useConnectionListeners = (account, subscriptions, users) => {
await notifier.notify(subscriptionId, notification, defaultClickAction);
}
};
const handleMessage = async (subscriptionId, message) => {
const subscription = await subscriptionManager.get(subscriptionId);
if (subscription.internal) {
await handleInternalMessage(message);
} else {
await handleNotification(subscriptionId, message);
}
};
connectionManager.registerStateListener(subscriptionManager.updateState);
connectionManager.registerMessageListener(handleMessage);
return () => {
connectionManager.resetStateListener();
connectionManager.resetMessageListener();