From c1244344294ddc875d0c300ad38ec0cb5a002269 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Fri, 11 Mar 2022 14:43:54 -0500 Subject: [PATCH] Migrate topics from old web ui; nicer stack traces --- web/src/components/App.js | 7 +- web/src/components/ErrorBoundary.js | 53 +++++++------ web/src/components/hooks.js | 117 +++++++++++++++++++--------- 3 files changed, 110 insertions(+), 67 deletions(-) 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); + }, []); +}