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:
		
							parent
							
								
									8319f1cf26
								
							
						
					
					
						commit
						59011c8a32
					
				
					 20 changed files with 369 additions and 351 deletions
				
			
		|  | @ -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); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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 /> | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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(); | ||||
|   }; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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", | ||||
|     }} | ||||
|   /> | ||||
| ); | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue