Просмотр исходного кода

Merge branch 'main' into switch-to-vite

binwiederhier 2 лет назад
Родитель
Сommit
d1e59fe08c

+ 5 - 1
docs/releases.md

@@ -1222,8 +1222,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 
 ### ntfy server v2.6.0 (UNRELEASED)
 
-**Bug fixes + maintenance:**
+**Bug fixes:**
 
 * Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
+
+**Maintenance:**
+
 * Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost))
 * Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost))
+* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost))

+ 6 - 0
web/.eslintrc

@@ -20,6 +20,12 @@
     "react/destructuring-assignment": "off",
     "react/jsx-no-useless-fragment": "off",
     "react/jsx-props-no-spreading": "off",
+    "react/jsx-no-duplicate-props": [
+      "error",
+      {
+        "ignoreCase": false // For <TextField>'s [iI]nputProps
+      }
+    ],
     "react/function-component-definition": [
       "error",
       {

+ 20 - 1
web/public/static/langs/fr.json

@@ -352,5 +352,24 @@
     "account_upgrade_dialog_interval_yearly_discount_save_up_to": "économisez jusqu'à {{discount}}%",
     "account_upgrade_dialog_tier_price_per_month": "mois",
     "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} prélevé annuellement. Économisez {{save}}.",
-    "account_upgrade_dialog_billing_contact_email": "Pour des questions concernant la facturation, merci de nous <Link>contacter</Link> directement."
+    "account_upgrade_dialog_billing_contact_email": "Pour des questions concernant la facturation, merci de nous <Link>contacter</Link> directement.",
+    "publish_dialog_call_label": "Appel téléphonique",
+    "account_basics_phone_numbers_title": "Numéros de téléphone",
+    "account_basics_phone_numbers_dialog_description": "Pour utiliser la fonctionnalité de notification par appels, vous devez ajouter et vérifier au moins un numéro de téléphone. La vérification peut se faire par SMS ou appel téléphonique.",
+    "account_basics_phone_numbers_description": "Pour des notifications par appel téléphoniques",
+    "account_basics_phone_numbers_no_phone_numbers_yet": "Pas encore de numéros de téléphone",
+    "account_basics_phone_numbers_copied_to_clipboard": "Numéro de téléphone copié dans le presse-papier",
+    "account_basics_phone_numbers_dialog_title": "Ajouter un numéro de téléphone",
+    "account_basics_phone_numbers_dialog_number_label": "Numéro de téléphone",
+    "account_basics_phone_numbers_dialog_number_placeholder": "Ex : +33701020304",
+    "account_basics_phone_numbers_dialog_verify_button_sms": "Envoyer un SMS",
+    "account_basics_phone_numbers_dialog_verify_button_call": "Appelez moi",
+    "account_basics_phone_numbers_dialog_code_label": "Code de vérification",
+    "account_basics_phone_numbers_dialog_code_placeholder": "Ex : 123456",
+    "account_basics_phone_numbers_dialog_check_verification_button": "Code de confirmarion",
+    "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+    "account_basics_phone_numbers_dialog_channel_call": "Appel",
+    "account_usage_calls_none": "Aucun appels téléphoniques ne peut être fait avec ce compte",
+    "publish_dialog_call_reset": "Supprimer les appels téléphoniques",
+    "publish_dialog_chip_call_label": "Appel téléphonique"
 }

+ 58 - 1
web/public/static/langs/uk.json

@@ -295,5 +295,62 @@
     "account_usage_messages_title": "Опубліковані повідомлення",
     "account_usage_emails_title": "Надіслані електронні листи",
     "account_usage_reservations_title": "Зарезервовані теми",
-    "account_usage_reservations_none": "Для цього облікового запису немає зарезервованих тем"
+    "account_usage_reservations_none": "Для цього облікового запису немає зарезервованих тем",
+    "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} на файл",
+    "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} загальне сховище",
+    "account_upgrade_dialog_tier_current_label": "Поточний",
+    "account_upgrade_dialog_tier_selected_label": "Вибране",
+    "account_upgrade_dialog_cancel_warning": "Це <strong> скасує вашу підписку</strong> і знизить версію вашого облікового запису {{date}}. У цю дату резервування тем, а також повідомлення, кешовані на сервері <strong>, буде видалено</strong>.",
+    "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} зарезервовані теми",
+    "account_upgrade_dialog_tier_features_no_reservations": "Немає зарезервованих тем",
+    "account_upgrade_dialog_tier_features_messages_other": "{{messages}} повідомлень в день",
+    "account_upgrade_dialog_tier_features_emails_one": "{{emails}} електронний лист в день",
+    "account_upgrade_dialog_tier_features_emails_other": "{{emails}} електронних листів в день",
+    "account_upgrade_dialog_tier_features_calls_one": "{{calls}} телефонний дзвінок в день",
+    "account_upgrade_dialog_tier_features_calls_other": "{{дзвінки}} телефонних дзвінків в день",
+    "account_upgrade_dialog_tier_features_no_calls": "Без телефонних дзвінків",
+    "account_upgrade_dialog_tier_price_per_month": "місяць",
+    "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} на рік. Рахунок виставляється щомісяця.",
+    "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} виставляється щорічно. Збережіть {{save}}.",
+    "account_upgrade_dialog_billing_contact_email": "Якщо у вас виникли запитання щодо оплати, <Link>зв’яжіться з нами</Link> безпосередньо.",
+    "account_upgrade_dialog_billing_contact_website": "Якщо у вас виникли запитання щодо оплати, відвідайте наш <Link>веб-сайт</Link>.",
+    "account_upgrade_dialog_button_cancel_subscription": "Скасувати підписку",
+    "account_upgrade_dialog_button_update_subscription": "Оновити підписку",
+    "account_tokens_title": "Токени доступу",
+    "account_tokens_table_expires_header": "Термін дії закінчується",
+    "account_tokens_description": "Використовуйте токени доступу при публікації та підписці через ntfy API, щоб не надсилати свої облікові дані. Ознайомтеся з <Link>документацією</Link>, щоб дізнатися більше.",
+    "account_tokens_table_token_header": "Токен",
+    "account_tokens_table_never_expires": "Ніколи не закінчується",
+    "account_tokens_table_label_header": "Мітка",
+    "account_tokens_table_current_session": "Поточний сеанс браузера",
+    "account_tokens_table_last_access_header": "Останній доступ",
+    "account_tokens_table_copied_to_clipboard": "Токен доступу скопійовано",
+    "account_tokens_table_cannot_delete_or_edit": "Неможливо редагувати або видалити токен поточного сеансу",
+    "account_tokens_table_create_token_button": "Створити токен доступу",
+    "account_tokens_table_last_origin_tooltip": "З IP-адреси {{ip}} натисніть для пошуку",
+    "account_tokens_dialog_title_create": "Створити токен доступу",
+    "account_tokens_dialog_button_cancel": "Скасувати",
+    "account_tokens_dialog_title_edit": "Редагувати токен доступу",
+    "account_tokens_dialog_title_delete": "Видалити токен доступу",
+    "account_tokens_dialog_label": "Мітка, наприклад, сповіщення Radarr",
+    "account_tokens_dialog_button_create": "Створити токен",
+    "account_tokens_dialog_button_update": "Оновити токен",
+    "account_tokens_dialog_expires_label": "Термін дії токену доступу закінчується через",
+    "account_tokens_dialog_expires_x_hours": "Термін дії токена закінчується через {{hours}} годин",
+    "account_tokens_dialog_expires_x_days": "Термін дії токена закінчується через {{days}} днів",
+    "account_tokens_delete_dialog_description": "Перш ніж видалити токен доступу, переконайтеся, що жодна програма або скрипт не використовує його. <strong>Ця дія не може бути скасована</strong>.",
+    "prefs_users_description_no_sync": "Користувачі та паролі не синхронізуються з вашим акаунтом.",
+    "prefs_users_table_cannot_delete_or_edit": "Неможливо видалити або відредагувати користувача, який увійшов у систему",
+    "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} зарезервована тема",
+    "account_upgrade_dialog_tier_features_messages_one": "{{messages}} повідомлення в день",
+    "account_tokens_dialog_expires_unchanged": "Залишити термін придатності без змін",
+    "account_tokens_dialog_expires_never": "Термін дії токена ніколи не закінчується",
+    "account_tokens_delete_dialog_title": "Видалити токен доступу",
+    "account_tokens_delete_dialog_submit_button": "Видалити токен назавжди",
+    "account_upgrade_dialog_proration_info": "<strong>Пропорція</strong>: При переході з одного тарифного плану на інший різниця в ціні буде <strong>списана негайно</strong>. При переході на нижчий рівень залишок коштів буде використано для оплати майбутніх розрахункових періодів.",
+    "account_upgrade_dialog_reservations_warning_one": "Обраний рівень дозволяє менше зарезервованих тем, ніж ваш поточний рівень. Перш ніж змінити свій рівень, <strong>будь ласка, видаліть принаймні одне резервування</strong>. Ви можете видалити резервування в <Link>Налаштуваннях</Link>.",
+    "account_upgrade_dialog_reservations_warning_other": "Обраний рівень дозволяє менше зарезервованих тем, ніж ваш поточний рівень. Перш ніж змінити свій рівень, <strong>будь ласка, видаліть принаймні {{count}} резервувань</strong>. Ви можете видалити резервування в <Link>Налаштуваннях</Link>.",
+    "account_upgrade_dialog_button_cancel": "Скасувати",
+    "account_upgrade_dialog_button_redirect_signup": "Зареєструватися зараз",
+    "account_upgrade_dialog_button_pay_now": "Оплатити зараз і підписатися"
 }

+ 4 - 1
web/src/components/Account.jsx

@@ -994,6 +994,7 @@ const TokenDialog = (props) => {
 
 const TokenDeleteDialog = (props) => {
   const { t } = useTranslation();
+  const [error, setError] = useState("");
 
   const handleSubmit = async () => {
     try {
@@ -1003,6 +1004,8 @@ const TokenDeleteDialog = (props) => {
       console.log(`[Account] Error deleting token`, e);
       if (e instanceof UnauthorizedError) {
         session.resetAndRedirect(routes.login);
+      } else {
+        setError(e.message);
       }
     }
   };
@@ -1015,7 +1018,7 @@ const TokenDeleteDialog = (props) => {
           <Trans i18nKey="account_tokens_delete_dialog_description" />
         </DialogContentText>
       </DialogContent>
-      <DialogFooter status>
+      <DialogFooter status={error}>
         <Button onClick={props.onClose}>{t("common_cancel")}</Button>
         <Button onClick={handleSubmit} color="error">
           {t("account_tokens_delete_dialog_submit_button")}

+ 2 - 3
web/src/components/App.jsx

@@ -27,14 +27,13 @@ export const AccountContext = createContext(null);
 
 const App = () => {
   const [account, setAccount] = useState(null);
-
-  const contextValue = useMemo(() => ({ account, setAccount }), [account, setAccount]);
+  const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]);
 
   return (
     <Suspense fallback={<Loader />}>
       <BrowserRouter>
         <ThemeProvider theme={theme}>
-          <AccountContext.Provider value={contextValue}>
+          <AccountContext.Provider value={accountMemo}>
             <CssBaseline />
             <ErrorBoundary>
               <Routes>

+ 2 - 0
web/src/components/EmojiPicker.jsx

@@ -74,6 +74,8 @@ 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")}>

+ 4 - 3
web/src/components/ErrorBoundary.jsx

@@ -45,9 +45,10 @@ class ErrorBoundaryImpl extends React.Component {
     // Fetch additional info and a better stack trace
     StackTrace.fromError(error).then((stack) => {
       console.error("[ErrorBoundary] Stacktrace fetched", stack);
-      const niceStack = `${error.toString()}\n${stack
-        .map((el) => `  at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`)
-        .join("\n")}`;
+      const stackString = stack
+          .map((el) => `  at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`)
+          .join("\n");
+      const niceStack = `${error.toString()}\n${stackString}`;
       this.setState({ niceStack });
     });
   }

+ 13 - 12
web/src/components/PublishDialog.jsx

@@ -383,23 +383,23 @@ const PublishDialog = (props) => {
                   "aria-label": t("publish_dialog_priority_label"),
                 }}
               >
-                {[5, 4, 3, 2, 1].map((priorityMenuItem) => (
+                {[5, 4, 3, 2, 1].map((p) => (
                   <MenuItem
-                    key={`priorityMenuItem${priorityMenuItem}`}
-                    value={priorityMenuItem}
+                    key={`priorityMenuItem${p}`}
+                    value={p}
                     aria-label={t("notifications_priority_x", {
-                      priority: priorityMenuItem,
+                      priority: p,
                     })}
                   >
                     <div style={{ display: "flex", alignItems: "center" }}>
                       <img
-                        src={priorities[priorityMenuItem].file}
+                        src={priorities[p].file}
                         style={{ marginRight: "8px" }}
                         alt={t("notifications_priority_x", {
-                          priority: priorityMenuItem,
+                          priority: p,
                         })}
                       />
-                      <div>{priorities[priorityMenuItem].label}</div>
+                      <div>{priorities[p].label}</div>
                     </div>
                   </MenuItem>
                 ))}
@@ -477,10 +477,8 @@ const PublishDialog = (props) => {
                     "aria-label": t("publish_dialog_call_label"),
                   }}
                 >
-                  {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}>
+                  {account?.phone_numbers?.map((phoneNumber) => (
+                    <MenuItem key={phoneNumber} value={phoneNumber} aria-label={phoneNumber}>
                       {t("publish_dialog_call_item", { number: phoneNumber })}
                     </MenuItem>
                   ))}
@@ -834,7 +832,10 @@ const ExpandingTextField = (props) => {
         variant="standard"
         sx={{ width: `${textWidth}px`, borderBottom: "none" }}
         InputProps={{
-          style: { fontSize: theme.typography[props.variant].fontSize, paddingBottom: 0, paddingTop: 0 },
+          style: { fontSize: theme.typography[props.variant].fontSize },
+        }}
+        inputProps={{
+          style: { paddingBottom: 0, paddingTop: 0 },
           "aria-label": props.placeholder,
         }}
         disabled={props.disabled}

+ 2 - 0
web/src/components/SubscriptionPopup.jsx

@@ -247,6 +247,8 @@ const DisplayNameDialog = (props) => {
           inputProps={{
             maxLength: 64,
             "aria-label": t("display_name_dialog_placeholder"),
+          }}
+          InputProps={{
             endAdornment: (
               <InputAdornment position="end">
                 <IconButton onClick={() => setDisplayName("")} edge="end">