1
0
Эх сурвалжийг харах

Migrate topics from old web ui; nicer stack traces

Philipp Heckel 3 жил өмнө
parent
commit
c124434429

+ 3 - 4
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 (

+ 28 - 25
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 (
                 <div style={{margin: '20px'}}>
                     <h2>Oh no, ntfy crashed 😮</h2>
@@ -44,21 +58,10 @@ class ErrorBoundary extends React.Component {
                         <Button variant="outlined" onClick={() => this.copyStack()}>Copy stack trace</Button>
                     </p>
                     <h3>Stack trace</h3>
-                    {this.state.stack
-                        ?
-                            <pre>
-                                {this.state.error && this.state.error.toString()}{"\n"}
-                                {this.state.stack}
-                            </pre>
-                        :
-                            <>
-                                <CircularProgress size="20px" sx={{verticalAlign: "text-bottom"}}/> Gather more info ...
-                            </>
-                    }
-                    <pre>
-                        {this.state.error && this.state.error.toString()}
-                        {this.state.info.componentStack}
-                    </pre>
+                    {this.state.niceStack
+                        ? <pre>{this.state.niceStack}</pre>
+                        : <><CircularProgress size="20px" sx={{verticalAlign: "text-bottom"}}/> Gather more info ...</>}
+                    <pre>{this.state.originalStack}</pre>
                 </div>
             );
         }

+ 79 - 38
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);
+    }, []);
+}