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 Toolbar from '@mui/material/Toolbar'; | ||||||
| import Notifications from "./Notifications"; | import Notifications from "./Notifications"; | ||||||
| import theme from "./theme"; | import theme from "./theme"; | ||||||
| import connectionManager from "../app/ConnectionManager"; |  | ||||||
| import Navigation from "./Navigation"; | import Navigation from "./Navigation"; | ||||||
| import ActionBar from "./ActionBar"; | import ActionBar from "./ActionBar"; | ||||||
| import notifier from "../app/Notifier"; | import notifier from "../app/Notifier"; | ||||||
|  | @ -18,7 +17,7 @@ import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from | ||||||
| import {expandUrl} from "../app/utils"; | import {expandUrl} from "../app/utils"; | ||||||
| import ErrorBoundary from "./ErrorBoundary"; | import ErrorBoundary from "./ErrorBoundary"; | ||||||
| import routes from "./routes"; | import routes from "./routes"; | ||||||
| import {useAutoSubscribe, useConnectionListeners} from "./hooks"; | import {useAutoSubscribe, useConnectionListeners, useLocalStorageMigration} from "./hooks"; | ||||||
| 
 | 
 | ||||||
| // TODO add drag and drop
 | // TODO add drag and drop
 | ||||||
| // TODO races when two tabs are open
 | // TODO races when two tabs are open
 | ||||||
|  | @ -67,8 +66,8 @@ const Layout = () => { | ||||||
|             || (window.location.origin === s.baseUrl && params.topic === s.topic) |             || (window.location.origin === s.baseUrl && params.topic === s.topic) | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     useConnectionListeners(); |     useConnectionListeners(subscriptions, users); | ||||||
|     useEffect(() => connectionManager.refresh(subscriptions, users), [subscriptions, users]); |     useLocalStorageMigration(); | ||||||
|     useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); |     useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  |  | ||||||
|  | @ -6,32 +6,46 @@ import Button from "@mui/material/Button"; | ||||||
| class ErrorBoundary extends React.Component { | class ErrorBoundary extends React.Component { | ||||||
|     constructor(props) { |     constructor(props) { | ||||||
|         super(props); |         super(props); | ||||||
|         this.state = { error: null, info: null, stack: null }; |         this.state = { | ||||||
|  |             error: false, | ||||||
|  |             originalStack: null, | ||||||
|  |             niceStack: null | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     componentDidCatch(error, info) { |     componentDidCatch(error, info) { | ||||||
|         this.setState({ error, info }); |  | ||||||
|         console.error("[ErrorBoundary] Error caught", 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 => { |         StackTrace.fromError(error).then(stack => { | ||||||
|             console.error("[ErrorBoundary] Stacktrace fetched", stack); |             console.error("[ErrorBoundary] Stacktrace fetched", stack); | ||||||
|             const stackStr = stack.map( el => { |             const niceStack = `${error.toString()}\n` + stack.map( el => `  at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n"); | ||||||
|                 return `  at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})\n`; |             this.setState({ niceStack }); | ||||||
|             }) |  | ||||||
|             this.setState({ stack: stackStr }) |  | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     copyStack() { |     copyStack() { | ||||||
|         let stack = ""; |         let stack = ""; | ||||||
|         if (this.state.stack) { |         if (this.state.niceStack) { | ||||||
|             stack += `Stack trace:\n${this.state.error}\n${this.state.stack}\n\n`; |             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); |         navigator.clipboard.writeText(stack); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     render() { |     render() { | ||||||
|         if (this.state.info) { |         if (this.state.error) { | ||||||
|             return ( |             return ( | ||||||
|                 <div style={{margin: '20px'}}> |                 <div style={{margin: '20px'}}> | ||||||
|                     <h2>Oh no, ntfy crashed 😮</h2> |                     <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> |                         <Button variant="outlined" onClick={() => this.copyStack()}>Copy stack trace</Button> | ||||||
|                     </p> |                     </p> | ||||||
|                     <h3>Stack trace</h3> |                     <h3>Stack trace</h3> | ||||||
|                     {this.state.stack |                     {this.state.niceStack | ||||||
|                         ? |                         ? <pre>{this.state.niceStack}</pre> | ||||||
|                             <pre> |                         : <><CircularProgress size="20px" sx={{verticalAlign: "text-bottom"}}/> Gather more info ...</>} | ||||||
|                                 {this.state.error && this.state.error.toString()}{"\n"} |                     <pre>{this.state.originalStack}</pre> | ||||||
|                                 {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> |  | ||||||
|                 </div> |                 </div> | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -7,46 +7,87 @@ import routes from "./routes"; | ||||||
| import connectionManager from "../app/ConnectionManager"; | import connectionManager from "../app/ConnectionManager"; | ||||||
| import poller from "../app/Poller"; | import poller from "../app/Poller"; | ||||||
| 
 | 
 | ||||||
| export const useConnectionListeners = () => { | /** | ||||||
|   const navigate = useNavigate(); |  * Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection | ||||||
|   useEffect(() => { |  * state changes. Conversely, when the subscription changes, the connection is refreshed (which may lead | ||||||
|         const handleNotification = async (subscriptionId, notification) => { |  * to the connection being re-established). | ||||||
|           const added = await subscriptionManager.addNotification(subscriptionId, notification); |  */ | ||||||
|           if (added) { | export const useConnectionListeners = (subscriptions, users) => { | ||||||
|             const defaultClickAction = (subscription) => navigate(routes.forSubscription(subscription)); |     const navigate = useNavigate(); | ||||||
|             await notifier.notify(subscriptionId, notification, defaultClickAction) | 
 | ||||||
|           } |     useEffect(() => { | ||||||
|         }; |             const handleNotification = async (subscriptionId, notification) => { | ||||||
|         connectionManager.registerStateListener(subscriptionManager.updateState); |                 const added = await subscriptionManager.addNotification(subscriptionId, notification); | ||||||
|         connectionManager.registerNotificationListener(handleNotification); |                 if (added) { | ||||||
|         return () => { |                     const defaultClickAction = (subscription) => navigate(routes.forSubscription(subscription)); | ||||||
|           connectionManager.resetStateListener(); |                     await notifier.notify(subscriptionId, notification, defaultClickAction) | ||||||
|           connectionManager.resetNotificationListener(); |                 } | ||||||
|         } |             }; | ||||||
|       }, |             connectionManager.registerStateListener(subscriptionManager.updateState); | ||||||
|       // We have to disable dep checking for "navigate". This is fine, it never changes.
 |             connectionManager.registerNotificationListener(handleNotification); | ||||||
|       // eslint-disable-next-line
 |             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) => { | export const useAutoSubscribe = (subscriptions, selected) => { | ||||||
|   const [hasRun, setHasRun] = useState(false); |     const [hasRun, setHasRun] = useState(false); | ||||||
|   const params = useParams(); |     const params = useParams(); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |     useEffect(() => { | ||||||
|     const loaded = subscriptions !== null && subscriptions !== undefined; |         const loaded = subscriptions !== null && subscriptions !== undefined; | ||||||
|     if (!loaded || hasRun) { |         if (!loaded || hasRun) { | ||||||
|       return; |             return; | ||||||
|     } |         } | ||||||
|     setHasRun(true); |         setHasRun(true); | ||||||
|     const eligible = params.topic && !selected && !disallowedTopic(params.topic); |         const eligible = params.topic && !selected && !disallowedTopic(params.topic); | ||||||
|     if (eligible) { |         if (eligible) { | ||||||
|       const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : window.location.origin; |             const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : window.location.origin; | ||||||
|       console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`); |             console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`); | ||||||
|       (async () => { |             (async () => { | ||||||
|         const subscription = await subscriptionManager.add(baseUrl, params.topic); |                 const subscription = await subscriptionManager.add(baseUrl, params.topic); | ||||||
|         poller.pollInBackground(subscription); // Dangle!
 |                 poller.pollInBackground(subscription); // Dangle!
 | ||||||
|       })(); |             })(); | ||||||
|     } |         } | ||||||
|   }, [params, subscriptions, selected, hasRun]); |     }, [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