Migrate topics from old web ui; nicer stack traces
This commit is contained in:
		
							parent
							
								
									0544a6f00d
								
							
						
					
					
						commit
						c124434429
					
				
					 3 changed files with 110 additions and 67 deletions
				
			
		|  | @ -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 ( | ||||
|  |  | |||
|  | @ -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> | ||||
|             ); | ||||
|         } | ||||
|  |  | |||
|  | @ -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); | ||||
|     }, []); | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue