Quellcode durchsuchen

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
-
nimbleghost vor 2 Jahren
Ursprung
Commit
59011c8a32

+ 6 - 5
web/src/app/AccountApi.js

@@ -261,12 +261,12 @@ class AccountApi {
 
   async createBillingSubscription(tier, interval) {
     console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`);
-    return await this.upsertBillingSubscription("POST", tier, interval);
+    return this.upsertBillingSubscription("POST", tier, interval);
   }
 
   async updateBillingSubscription(tier, interval) {
     console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`);
-    return await this.upsertBillingSubscription("PUT", tier, interval);
+    return this.upsertBillingSubscription("PUT", tier, interval);
   }
 
   async upsertBillingSubscription(method, tier, interval) {
@@ -279,7 +279,7 @@ class AccountApi {
         interval,
       }),
     });
-    return await response.json(); // May throw SyntaxError
+    return response.json(); // May throw SyntaxError
   }
 
   async deleteBillingSubscription() {
@@ -298,7 +298,7 @@ class AccountApi {
       method: "POST",
       headers: withBearerAuth({}, session.token()),
     });
-    return await response.json(); // May throw SyntaxError
+    return response.json(); // May throw SyntaxError
   }
 
   async verifyPhoneNumber(phoneNumber, channel) {
@@ -327,7 +327,7 @@ class AccountApi {
     });
   }
 
-  async deletePhoneNumber(phoneNumber, code) {
+  async deletePhoneNumber(phoneNumber) {
     const url = accountPhoneUrl(config.base_url);
     console.log(`[AccountApi] Deleting phone number ${url}`);
     await fetchOrThrow(url, {
@@ -369,6 +369,7 @@ class AccountApi {
       if (e instanceof UnauthorizedError) {
         session.resetAndRedirect(routes.login);
       }
+      return undefined;
     }
   }
 

+ 8 - 7
web/src/app/Connection.js

@@ -1,7 +1,14 @@
+/* eslint-disable max-classes-per-file */
 import { basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs } from "./utils";
 
 const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
 
+export class ConnectionState {
+  static Connected = "connected";
+
+  static Connecting = "connecting";
+}
+
 /**
  * A connection contains a single WebSocket connection for one topic. It handles its connection
  * status itself, including reconnect attempts and backoff.
@@ -63,7 +70,7 @@ class Connection {
         this.ws = null;
       } else {
         const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length - 1)];
-        this.retryCount++;
+        this.retryCount += 1;
         console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`);
         this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000);
         this.onStateChanged(this.subscriptionId, ConnectionState.Connecting);
@@ -108,10 +115,4 @@ class Connection {
   }
 }
 
-export class ConnectionState {
-  static Connected = "connected";
-
-  static Connecting = "connecting";
-}
-
 export default Connection;

+ 5 - 5
web/src/app/ConnectionManager.js

@@ -1,6 +1,9 @@
 import Connection from "./Connection";
 import { hashCode } from "./utils";
 
+const makeConnectionId = async (subscription, user) =>
+  user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`);
+
 /**
  * The connection manager keeps track of active connections (WebSocket connections, see Connection).
  *
@@ -69,8 +72,8 @@ class ConnectionManager {
           topic,
           user,
           since,
-          (subscriptionId, notification) => this.notificationReceived(subscriptionId, notification),
-          (subscriptionId, state) => this.stateChanged(subscriptionId, state)
+          (subId, notification) => this.notificationReceived(subId, notification),
+          (subId, state) => this.stateChanged(subId, state)
         );
         this.connections.set(connectionId, connection);
         console.log(
@@ -112,8 +115,5 @@ class ConnectionManager {
   }
 }
 
-const makeConnectionId = async (subscription, user) =>
-  user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`);
-
 const connectionManager = new ConnectionManager();
 export default connectionManager;

+ 2 - 2
web/src/app/Notifier.js

@@ -29,7 +29,7 @@ class Notifier {
       icon: logo,
     });
     if (notification.click) {
-      n.onclick = (e) => openUrl(notification.click);
+      n.onclick = () => openUrl(notification.click);
     } else {
       n.onclick = () => onClickFallback(subscription);
     }
@@ -87,7 +87,7 @@ class Notifier {
    * is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification
    */
   contextSupported() {
-    return location.protocol === "https:" || location.hostname.match("^127.") || location.hostname === "localhost";
+    return window.location.protocol === "https:" || window.location.hostname.match("^127.") || window.location.hostname === "localhost";
   }
 }
 

+ 2 - 0
web/src/app/Poller.js

@@ -23,6 +23,8 @@ class Poller {
     const subscriptions = await subscriptionManager.all();
     for (const s of subscriptions) {
       try {
+        // TODO(eslint): Switch to Promise.all
+        // eslint-disable-next-line no-await-in-loop
         await this.poll(s);
       } catch (e) {
         console.log(`[Poller] Error polling ${s.id}`, e);

+ 11 - 3
web/src/app/SubscriptionManager.js

@@ -7,6 +7,7 @@ class SubscriptionManager {
     const subscriptions = await db.subscriptions.toArray();
     await Promise.all(
       subscriptions.map(async (s) => {
+        // eslint-disable-next-line no-param-reassign
         s.new = await db.notifications.where({ subscriptionId: s.id, new: 1 }).count();
       })
     );
@@ -14,7 +15,7 @@ class SubscriptionManager {
   }
 
   async get(subscriptionId) {
-    return await db.subscriptions.get(subscriptionId);
+    return db.subscriptions.get(subscriptionId);
   }
 
   async add(baseUrl, topic, internal) {
@@ -40,10 +41,14 @@ class SubscriptionManager {
 
     // Add remote subscriptions
     const remoteIds = []; // = topicUrl(baseUrl, topic)
-    for (let i = 0; i < remoteSubscriptions.length; i++) {
+    for (let i = 0; i < remoteSubscriptions.length; i += 1) {
       const remote = remoteSubscriptions[i];
+      // TODO(eslint): Switch to Promise.all
+      // eslint-disable-next-line no-await-in-loop
       const local = await this.add(remote.base_url, remote.topic, false);
       const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null;
+      // TODO(eslint): Switch to Promise.all
+      // eslint-disable-next-line no-await-in-loop
       await this.update(local.id, {
         displayName: remote.display_name, // May be undefined
         reservation, // May be null!
@@ -53,10 +58,12 @@ class SubscriptionManager {
 
     // Remove local subscriptions that do not exist remotely
     const localSubscriptions = await db.subscriptions.toArray();
-    for (let i = 0; i < localSubscriptions.length; i++) {
+    for (let i = 0; i < localSubscriptions.length; i += 1) {
       const local = localSubscriptions[i];
       const remoteExists = remoteIds.includes(local.id);
       if (!local.internal && !remoteExists) {
+        // TODO(eslint): Switch to Promise.all
+        // eslint-disable-next-line no-await-in-loop
         await this.remove(local.id);
       }
     }
@@ -101,6 +108,7 @@ class SubscriptionManager {
       return false;
     }
     try {
+      // eslint-disable-next-line no-param-reassign
       notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
       await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab
       await db.subscriptions.update(subscriptionId, {

+ 33 - 32
web/src/app/errors.js

@@ -1,37 +1,6 @@
+/* eslint-disable max-classes-per-file */
 // This is a subset of, and the counterpart to errors.go
 
-export const fetchOrThrow = async (url, options) => {
-  const response = await fetch(url, options);
-  if (response.status !== 200) {
-    await throwAppError(response);
-  }
-  return response; // Promise!
-};
-
-export const throwAppError = async (response) => {
-  if (response.status === 401 || response.status === 403) {
-    console.log(`[Error] HTTP ${response.status}`, response);
-    throw new UnauthorizedError();
-  }
-  const error = await maybeToJson(response);
-  if (error?.code) {
-    console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response);
-    if (error.code === UserExistsError.CODE) {
-      throw new UserExistsError();
-    } else if (error.code === TopicReservedError.CODE) {
-      throw new TopicReservedError();
-    } else if (error.code === AccountCreateLimitReachedError.CODE) {
-      throw new AccountCreateLimitReachedError();
-    } else if (error.code === IncorrectPasswordError.CODE) {
-      throw new IncorrectPasswordError();
-    } else if (error?.error) {
-      throw new Error(`Error ${error.code}: ${error.error}`);
-    }
-  }
-  console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response);
-  throw new Error(`Unexpected response ${response.status}`);
-};
-
 const maybeToJson = async (response) => {
   try {
     return await response.json();
@@ -77,3 +46,35 @@ export class IncorrectPasswordError extends Error {
     super("Password incorrect");
   }
 }
+
+export const throwAppError = async (response) => {
+  if (response.status === 401 || response.status === 403) {
+    console.log(`[Error] HTTP ${response.status}`, response);
+    throw new UnauthorizedError();
+  }
+  const error = await maybeToJson(response);
+  if (error?.code) {
+    console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response);
+    if (error.code === UserExistsError.CODE) {
+      throw new UserExistsError();
+    } else if (error.code === TopicReservedError.CODE) {
+      throw new TopicReservedError();
+    } else if (error.code === AccountCreateLimitReachedError.CODE) {
+      throw new AccountCreateLimitReachedError();
+    } else if (error.code === IncorrectPasswordError.CODE) {
+      throw new IncorrectPasswordError();
+    } else if (error?.error) {
+      throw new Error(`Error ${error.code}: ${error.error}`);
+    }
+  }
+  console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response);
+  throw new Error(`Unexpected response ${response.status}`);
+};
+
+export const fetchOrThrow = async (url, options) => {
+  const response = await fetch(url, options);
+  if (response.status !== 200) {
+    await throwAppError(response);
+  }
+  return response; // Promise!
+};

+ 43 - 40
web/src/app/utils.js

@@ -9,6 +9,10 @@ import pop from "../sounds/pop.mp3";
 import popSwoosh from "../sounds/pop-swoosh.mp3";
 import config from "./config";
 
+export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
+export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
+export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
+export const expandSecureUrl = (url) => `https://${url}`;
 export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
 export const topicUrlWs = (baseUrl, topic) =>
   `${topicUrl(baseUrl, topic)}/ws`.replaceAll("https://", "wss://").replaceAll("http://", "ws://");
@@ -28,13 +32,11 @@ export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account
 export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
 export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
 export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`;
-export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
-export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
-export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
-export const expandSecureUrl = (url) => `https://${url}`;
 
 export const validUrl = (url) => url.match(/^https?:\/\/.+/);
 
+export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic);
+
 export const validTopic = (topic) => {
   if (disallowedTopic(topic)) {
     return false;
@@ -42,8 +44,6 @@ export const validTopic = (topic) => {
   return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app!
 };
 
-export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic);
-
 export const topicDisplayName = (subscription) => {
   if (subscription.displayName) {
     return subscription.displayName;
@@ -67,13 +67,6 @@ const toEmojis = (tags) => {
   return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]);
 };
 
-export const formatTitleWithDefault = (m, fallback) => {
-  if (m.title) {
-    return formatTitle(m);
-  }
-  return fallback;
-};
-
 export const formatTitle = (m) => {
   const emojiList = toEmojis(m.tags);
   if (emojiList.length > 0) {
@@ -82,6 +75,13 @@ export const formatTitle = (m) => {
   return m.title;
 };
 
+export const formatTitleWithDefault = (m, fallback) => {
+  if (m.title) {
+    return formatTitle(m);
+  }
+  return fallback;
+};
+
 export const formatMessage = (m) => {
   if (m.title) {
     return m.message;
@@ -98,15 +98,15 @@ export const unmatchedTags = (tags) => {
   return tags.filter((tag) => !(tag in emojis));
 };
 
-export const maybeWithAuth = (headers, user) => {
-  if (user && user.password) {
-    return withBasicAuth(headers, user.username, user.password);
-  }
-  if (user && user.token) {
-    return withBearerAuth(headers, user.token);
-  }
-  return headers;
-};
+export const encodeBase64 = (s) => Base64.encode(s);
+
+export const encodeBase64Url = (s) => Base64.encodeURI(s);
+
+export const bearerAuth = (token) => `Bearer ${token}`;
+
+export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`;
+
+export const withBearerAuth = (headers, token) => ({ ...headers, Authorization: bearerAuth(token) });
 
 export const maybeWithBearerAuth = (headers, token) => {
   if (token) {
@@ -115,24 +115,18 @@ export const maybeWithBearerAuth = (headers, token) => {
   return headers;
 };
 
-export const withBasicAuth = (headers, username, password) => {
-  headers.Authorization = basicAuth(username, password);
-  return headers;
-};
-
-export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`;
+export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) });
 
-export const withBearerAuth = (headers, token) => {
-  headers.Authorization = bearerAuth(token);
+export const maybeWithAuth = (headers, user) => {
+  if (user && user.password) {
+    return withBasicAuth(headers, user.username, user.password);
+  }
+  if (user && user.token) {
+    return withBearerAuth(headers, user.token);
+  }
   return headers;
 };
 
-export const bearerAuth = (token) => `Bearer ${token}`;
-
-export const encodeBase64 = (s) => Base64.encode(s);
-
-export const encodeBase64Url = (s) => Base64.encodeURI(s);
-
 export const maybeAppendActionErrors = (message, notification) => {
   const actionErrors = (notification.actions ?? [])
     .map((action) => action.error)
@@ -147,10 +141,12 @@ export const maybeAppendActionErrors = (message, notification) => {
 export const shuffle = (arr) => {
   let j;
   let x;
-  for (let index = arr.length - 1; index > 0; index--) {
+  for (let index = arr.length - 1; index > 0; index -= 1) {
     j = Math.floor(Math.random() * (index + 1));
     x = arr[index];
+    // eslint-disable-next-line no-param-reassign
     arr[index] = arr[j];
+    // eslint-disable-next-line no-param-reassign
     arr[j] = x;
   }
   return arr;
@@ -165,9 +161,11 @@ export const splitNoEmpty = (s, delimiter) =>
 /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */
 export const hashCode = async (s) => {
   let hash = 0;
-  for (let i = 0; i < s.length; i++) {
+  for (let i = 0; i < s.length; i += 1) {
     const char = s.charCodeAt(i);
+    // eslint-disable-next-line no-bitwise
     hash = (hash << 5) - hash + char;
+    // eslint-disable-next-line no-bitwise
     hash &= hash; // Convert to 32bit integer
   }
   return hash;
@@ -248,6 +246,7 @@ export const playSound = async (id) => {
 };
 
 // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
+// eslint-disable-next-line func-style
 export async function* fetchLinesIterator(fileURL, headers) {
   const utf8Decoder = new TextDecoder("utf-8");
   const response = await fetch(fileURL, {
@@ -267,9 +266,12 @@ export async function* fetchLinesIterator(fileURL, headers) {
         break;
       }
       const remainder = chunk.substr(startIndex);
+      // eslint-disable-next-line no-await-in-loop
       ({ value: chunk, done: readerDone } = await reader.read());
       chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : "");
-      startIndex = re.lastIndex = 0;
+      startIndex = 0;
+      re.lastIndex = 0;
+      // eslint-disable-next-line no-continue
       continue;
     }
     yield chunk.substring(startIndex, result.index);
@@ -283,7 +285,8 @@ export async function* fetchLinesIterator(fileURL, headers) {
 export const randomAlphanumericString = (len) => {
   const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
   let id = "";
-  for (let i = 0; i < len; i++) {
+  for (let i = 0; i < len; i += 1) {
+    // eslint-disable-next-line no-bitwise
     id += alphabet[(Math.random() * alphabet.length) | 0];
   }
   return id;

+ 17 - 24
web/src/components/Account.jsx

@@ -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);
       }
     }
   };

+ 11 - 8
web/src/components/App.jsx

@@ -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;

+ 12 - 14
web/src/components/EmojiPicker.jsx

@@ -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;

+ 10 - 10
web/src/components/ErrorBoundary.jsx

@@ -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

+ 4 - 4
web/src/components/Navigation.jsx

@@ -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);

+ 82 - 74
web/src/components/Notifications.jsx

@@ -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 />

+ 19 - 15
web/src/components/Preferences.jsx

@@ -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;

+ 30 - 31
web/src/components/PublishDialog.jsx

@@ -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();
   };

+ 15 - 15
web/src/components/SubscribeDialog.jsx

@@ -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;

+ 14 - 19
web/src/components/SubscriptionPopup.jsx

@@ -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",
+    }}
+  />
+);

+ 32 - 32
web/src/components/UpgradeDialog.jsx

@@ -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;

+ 11 - 9
web/src/components/hooks.js

@@ -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();