diff --git a/web/src/components/App.js b/web/src/components/App.js
index 872681fc..4782a26c 100644
--- a/web/src/components/App.js
+++ b/web/src/components/App.js
@@ -6,7 +6,6 @@ import CssBaseline from '@mui/material/CssBaseline';
import Toolbar from '@mui/material/Toolbar';
import Notifications from "./Notifications";
import theme from "./theme";
-import connectionManager from "../app/ConnectionManager";
import Navigation from "./Navigation";
import ActionBar from "./ActionBar";
import notifier from "../app/Notifier";
@@ -18,7 +17,7 @@ import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from
import {expandUrl} from "../app/utils";
import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes";
-import {useAutoSubscribe, useConnectionListeners} from "./hooks";
+import {useAutoSubscribe, useConnectionListeners, useLocalStorageMigration} from "./hooks";
// TODO add drag and drop
// TODO races when two tabs are open
@@ -67,8 +66,8 @@ const Layout = () => {
|| (window.location.origin === s.baseUrl && params.topic === s.topic)
});
- useConnectionListeners();
- useEffect(() => connectionManager.refresh(subscriptions, users), [subscriptions, users]);
+ useConnectionListeners(subscriptions, users);
+ useLocalStorageMigration();
useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]);
return (
diff --git a/web/src/components/ErrorBoundary.js b/web/src/components/ErrorBoundary.js
index aa63d2fc..d309f4b0 100644
--- a/web/src/components/ErrorBoundary.js
+++ b/web/src/components/ErrorBoundary.js
@@ -6,32 +6,46 @@ import Button from "@mui/material/Button";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
- this.state = { error: null, info: null, stack: null };
+ this.state = {
+ error: false,
+ originalStack: null,
+ niceStack: null
+ };
}
componentDidCatch(error, info) {
- this.setState({ error, info });
console.error("[ErrorBoundary] Error caught", error, info);
+
+ // Immediately render original stack trace
+ const prettierOriginalStack = info.componentStack
+ .trim()
+ .split("\n")
+ .map(line => ` at ${line}`)
+ .join("\n");
+ this.setState({
+ error: true,
+ originalStack: `${error.toString()}\n${prettierOriginalStack}`
+ });
+
+ // Fetch additional info and a better stack trace
StackTrace.fromError(error).then(stack => {
console.error("[ErrorBoundary] Stacktrace fetched", stack);
- const stackStr = stack.map( el => {
- return ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})\n`;
- })
- this.setState({ stack: stackStr })
+ const niceStack = `${error.toString()}\n` + stack.map( el => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n");
+ this.setState({ niceStack });
});
}
copyStack() {
let stack = "";
- if (this.state.stack) {
- stack += `Stack trace:\n${this.state.error}\n${this.state.stack}\n\n`;
+ if (this.state.niceStack) {
+ stack += `${this.state.niceStack}\n\n`;
}
- stack += `Original stack trace:\n${this.state.error}\n${this.state.info.componentStack}\n\n`;
+ stack += `${this.state.originalStack}\n`;
navigator.clipboard.writeText(stack);
}
render() {
- if (this.state.info) {
+ if (this.state.error) {
return (
Oh no, ntfy crashed 😮
@@ -44,21 +58,10 @@ class ErrorBoundary extends React.Component {
Stack trace
- {this.state.stack
- ?
-
- {this.state.error && this.state.error.toString()}{"\n"}
- {this.state.stack}
-
- :
- <>
-
Gather more info ...
- >
- }
-
- {this.state.error && this.state.error.toString()}
- {this.state.info.componentStack}
-
+ {this.state.niceStack
+ ?
{this.state.niceStack}
+ : <>
Gather more info ...>}
+
{this.state.originalStack}
);
}
diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js
index b0f8787a..f3299856 100644
--- a/web/src/components/hooks.js
+++ b/web/src/components/hooks.js
@@ -7,46 +7,87 @@ import routes from "./routes";
import connectionManager from "../app/ConnectionManager";
import poller from "../app/Poller";
-export const useConnectionListeners = () => {
- const navigate = useNavigate();
- useEffect(() => {
- const handleNotification = async (subscriptionId, notification) => {
- const added = await subscriptionManager.addNotification(subscriptionId, notification);
- if (added) {
- const defaultClickAction = (subscription) => navigate(routes.forSubscription(subscription));
- await notifier.notify(subscriptionId, notification, defaultClickAction)
- }
- };
- connectionManager.registerStateListener(subscriptionManager.updateState);
- connectionManager.registerNotificationListener(handleNotification);
- return () => {
- connectionManager.resetStateListener();
- connectionManager.resetNotificationListener();
- }
- },
- // We have to disable dep checking for "navigate". This is fine, it never changes.
- // eslint-disable-next-line
- []);
+/**
+ * Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection
+ * state changes. Conversely, when the subscription changes, the connection is refreshed (which may lead
+ * to the connection being re-established).
+ */
+export const useConnectionListeners = (subscriptions, users) => {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const handleNotification = async (subscriptionId, notification) => {
+ const added = await subscriptionManager.addNotification(subscriptionId, notification);
+ if (added) {
+ const defaultClickAction = (subscription) => navigate(routes.forSubscription(subscription));
+ await notifier.notify(subscriptionId, notification, defaultClickAction)
+ }
+ };
+ connectionManager.registerStateListener(subscriptionManager.updateState);
+ connectionManager.registerNotificationListener(handleNotification);
+ return () => {
+ connectionManager.resetStateListener();
+ connectionManager.resetNotificationListener();
+ }
+ },
+ // We have to disable dep checking for "navigate". This is fine, it never changes.
+ // eslint-disable-next-line
+ []
+ );
+
+ useEffect(() => {
+ connectionManager.refresh(subscriptions, users); // Dangle
+ }, [subscriptions, users]);
};
+/**
+ * Automatically adds a subscription if we navigate to a page that has not been subscribed to.
+ * This will only be run once after the initial page load.
+ */
export const useAutoSubscribe = (subscriptions, selected) => {
- const [hasRun, setHasRun] = useState(false);
- const params = useParams();
+ const [hasRun, setHasRun] = useState(false);
+ const params = useParams();
- useEffect(() => {
- const loaded = subscriptions !== null && subscriptions !== undefined;
- if (!loaded || hasRun) {
- return;
- }
- setHasRun(true);
- const eligible = params.topic && !selected && !disallowedTopic(params.topic);
- if (eligible) {
- const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : window.location.origin;
- console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
- (async () => {
- const subscription = await subscriptionManager.add(baseUrl, params.topic);
- poller.pollInBackground(subscription); // Dangle!
- })();
- }
- }, [params, subscriptions, selected, hasRun]);
+ useEffect(() => {
+ const loaded = subscriptions !== null && subscriptions !== undefined;
+ if (!loaded || hasRun) {
+ return;
+ }
+ setHasRun(true);
+ const eligible = params.topic && !selected && !disallowedTopic(params.topic);
+ if (eligible) {
+ const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : window.location.origin;
+ console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
+ (async () => {
+ const subscription = await subscriptionManager.add(baseUrl, params.topic);
+ poller.pollInBackground(subscription); // Dangle!
+ })();
+ }
+ }, [params, subscriptions, selected, hasRun]);
};
+
+export const useLocalStorageMigration = () => {
+ const [hasRun, setHasRun] = useState(false);
+ useEffect(() => {
+ if (hasRun) {
+ return;
+ }
+ const topicsStr = localStorage.getItem("topics");
+ if (topicsStr) {
+ const topics = topicsStr
+ .split(",")
+ .filter(topic => topic !== "");
+ if (topics.length > 0) {
+ (async () => {
+ for (const topic of topics) {
+ const baseUrl = window.location.origin;
+ const subscription = await subscriptionManager.add(baseUrl, topic);
+ poller.pollInBackground(subscription); // Dangle!
+ }
+ localStorage.removeItem("topics");
+ })();
+ }
+ }
+ setHasRun(true);
+ }, []);
+}