Connection lost banner (#1101)
* connection lost banner if there is no connection to the home server Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
		
					parent
					
						
							
								cabad628b4
							
						
					
				
			
			
				commit
				
					
						2ffe000bf5
					
				
			
		
					 7 changed files with 130 additions and 5 deletions
				
			
		|  | @ -36,6 +36,7 @@ | |||
|   "Close": "Close", | ||||
|   "Confirm password": "Confirm password", | ||||
|   "Connection lost": "Connection lost", | ||||
|   "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", | ||||
|   "Copied!": "Copied!", | ||||
|   "Copy": "Copy", | ||||
|   "Copy and share this call link": "Copy and share this call link", | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ import { usePageFocusStyle } from "./usePageFocusStyle"; | |||
| import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage"; | ||||
| import { InspectorContextProvider } from "./room/GroupCallInspector"; | ||||
| import { CrashView, LoadingView } from "./FullScreenView"; | ||||
| import { DisconnectedBanner } from "./DisconnectedBanner"; | ||||
| import { Initializer } from "./initializer"; | ||||
| import { MediaHandlerProvider } from "./settings/useMediaHandler"; | ||||
| 
 | ||||
|  | @ -60,6 +61,7 @@ export default function App({ history }: AppProps) { | |||
|               <InspectorContextProvider> | ||||
|                 <Sentry.ErrorBoundary fallback={errorPage}> | ||||
|                   <OverlayProvider> | ||||
|                     <DisconnectedBanner /> | ||||
|                     <Switch> | ||||
|                       <SentryRoute exact path="/"> | ||||
|                         <HomePage /> | ||||
|  |  | |||
|  | @ -25,9 +25,10 @@ import React, { | |||
|   useRef, | ||||
| } from "react"; | ||||
| import { useHistory } from "react-router-dom"; | ||||
| import { MatrixClient } from "matrix-js-sdk/src/client"; | ||||
| import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; | ||||
| import { logger } from "matrix-js-sdk/src/logger"; | ||||
| import { useTranslation } from "react-i18next"; | ||||
| import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; | ||||
| 
 | ||||
| import { ErrorView } from "./FullScreenView"; | ||||
| import { | ||||
|  | @ -70,6 +71,8 @@ const loadSession = (): Session => { | |||
| const saveSession = (session: Session) => | ||||
|   localStorage.setItem("matrix-auth-store", JSON.stringify(session)); | ||||
| const clearSession = () => localStorage.removeItem("matrix-auth-store"); | ||||
| const isDisconnected = (syncState, syncData) => | ||||
|   syncState === "ERROR" && syncData?.error?.name === "ConnectionError"; | ||||
| 
 | ||||
| interface ClientState { | ||||
|   loading: boolean; | ||||
|  | @ -81,6 +84,7 @@ interface ClientState { | |||
|   logout: () => void; | ||||
|   setClient: (client: MatrixClient, session: Session) => void; | ||||
|   error?: Error; | ||||
|   disconnected: boolean; | ||||
| } | ||||
| 
 | ||||
| const ClientContext = createContext<ClientState>(null); | ||||
|  | @ -98,7 +102,15 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|   const history = useHistory(); | ||||
|   const initializing = useRef(false); | ||||
|   const [ | ||||
|     { loading, isAuthenticated, isPasswordlessUser, client, userName, error }, | ||||
|     { | ||||
|       loading, | ||||
|       isAuthenticated, | ||||
|       isPasswordlessUser, | ||||
|       client, | ||||
|       userName, | ||||
|       error, | ||||
|       disconnected, | ||||
|     }, | ||||
|     setState, | ||||
|   ] = useState<ClientProviderState>({ | ||||
|     loading: true, | ||||
|  | @ -107,8 +119,18 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|     client: undefined, | ||||
|     userName: null, | ||||
|     error: undefined, | ||||
|     disconnected: false, | ||||
|   }); | ||||
| 
 | ||||
|   const onSync = (state: SyncState, _old: SyncState, data: ISyncStateData) => { | ||||
|     setState((currentState) => { | ||||
|       const disconnected = isDisconnected(state, data); | ||||
|       return disconnected === currentState.disconnected | ||||
|         ? currentState | ||||
|         : { ...currentState, disconnected }; | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     // In case the component is mounted, unmounted, and remounted quickly (as
 | ||||
|     // React does in strict mode), we need to make sure not to doubly initialize
 | ||||
|  | @ -183,9 +205,10 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|         } | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     let clientWithListener: MatrixClient; | ||||
|     init() | ||||
|       .then(({ client, isPasswordlessUser }) => { | ||||
|         clientWithListener = client; | ||||
|         setState({ | ||||
|           client, | ||||
|           loading: false, | ||||
|  | @ -193,7 +216,12 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|           isPasswordlessUser, | ||||
|           userName: client?.getUserIdLocalpart(), | ||||
|           error: undefined, | ||||
|           disconnected: isDisconnected( | ||||
|             client?.getSyncState, | ||||
|             client?.getSyncStateData | ||||
|           ), | ||||
|         }); | ||||
|         clientWithListener?.on(ClientEvent.Sync, onSync); | ||||
|       }) | ||||
|       .catch((err) => { | ||||
|         logger.error(err); | ||||
|  | @ -204,9 +232,13 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|           isPasswordlessUser: false, | ||||
|           userName: null, | ||||
|           error: undefined, | ||||
|           disconnected: false, | ||||
|         }); | ||||
|       }) | ||||
|       .finally(() => (initializing.current = false)); | ||||
|     return () => { | ||||
|       clientWithListener?.removeListener(ClientEvent.Sync, onSync); | ||||
|     }; | ||||
|   }, []); | ||||
| 
 | ||||
|   const changePassword = useCallback( | ||||
|  | @ -235,6 +267,7 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|         isPasswordlessUser: false, | ||||
|         userName: client.getUserIdLocalpart(), | ||||
|         error: undefined, | ||||
|         disconnected: false, | ||||
|       }); | ||||
|     }, | ||||
|     [client] | ||||
|  | @ -256,6 +289,10 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|           isPasswordlessUser: session.passwordlessUser, | ||||
|           userName: newClient.getUserIdLocalpart(), | ||||
|           error: undefined, | ||||
|           disconnected: isDisconnected( | ||||
|             newClient.getSyncState(), | ||||
|             newClient.getSyncStateData() | ||||
|           ), | ||||
|         }); | ||||
|       } else { | ||||
|         clearSession(); | ||||
|  | @ -267,6 +304,7 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|           isPasswordlessUser: false, | ||||
|           userName: null, | ||||
|           error: undefined, | ||||
|           disconnected: false, | ||||
|         }); | ||||
|       } | ||||
|     }, | ||||
|  | @ -284,6 +322,7 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|       isPasswordlessUser: true, | ||||
|       userName: "", | ||||
|       error: undefined, | ||||
|       disconnected: false, | ||||
|     }); | ||||
|     history.push("/"); | ||||
|     PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest); | ||||
|  | @ -326,6 +365,7 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|       userName, | ||||
|       setClient, | ||||
|       error: undefined, | ||||
|       disconnected, | ||||
|     }), | ||||
|     [ | ||||
|       loading, | ||||
|  | @ -336,6 +376,7 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|       logout, | ||||
|       userName, | ||||
|       setClient, | ||||
|       disconnected, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										27
									
								
								src/DisconnectedBanner.module.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/DisconnectedBanner.module.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| /* | ||||
| Copyright 2023 New Vector Ltd | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
| http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| .banner { | ||||
|   position: absolute; | ||||
|   padding: 29px; | ||||
|   background-color: var(--quaternary-content); | ||||
|   vertical-align: middle; | ||||
|   font-size: var(--font-size-body); | ||||
|   text-align: center; | ||||
|   z-index: 1; | ||||
|   top: 76px; | ||||
|   width: calc(100% - 58px); | ||||
| } | ||||
							
								
								
									
										47
									
								
								src/DisconnectedBanner.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/DisconnectedBanner.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | |||
| /* | ||||
| Copyright 2023 New Vector Ltd | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
| http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import classNames from "classnames"; | ||||
| import React, { HTMLAttributes, ReactNode } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | ||||
| 
 | ||||
| import styles from "./DisconnectedBanner.module.css"; | ||||
| import { useClient } from "./ClientContext"; | ||||
| 
 | ||||
| interface DisconnectedBannerProps extends HTMLAttributes<HTMLElement> { | ||||
|   children?: ReactNode; | ||||
|   className?: string; | ||||
| } | ||||
| 
 | ||||
| export function DisconnectedBanner({ | ||||
|   children, | ||||
|   className, | ||||
|   ...rest | ||||
| }: DisconnectedBannerProps) { | ||||
|   const { t } = useTranslation(); | ||||
|   const { disconnected } = useClient(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {disconnected && ( | ||||
|         <div className={classNames(styles.banner, className)} {...rest}> | ||||
|           {children} | ||||
|           {t("Connectivity to the server has been lost.")} | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | @ -45,6 +45,12 @@ class DependencyLoadStates { | |||
| 
 | ||||
| export class Initializer { | ||||
|   private static internalInstance: Initializer; | ||||
|   private isInitialized = false; | ||||
| 
 | ||||
|   public static isInitialized(): boolean { | ||||
|     return Initializer.internalInstance?.isInitialized; | ||||
|   } | ||||
| 
 | ||||
|   public static initBeforeReact() { | ||||
|     // this maybe also needs to return a promise in the future,
 | ||||
|     // if we have to do async inits before showing the loading screen
 | ||||
|  | @ -223,6 +229,7 @@ export class Initializer { | |||
|     if (this.loadStates.allDepsAreLoaded()) { | ||||
|       // resolve if there is no dependency that is not loaded
 | ||||
|       resolve(); | ||||
|       this.isInitialized = true; | ||||
|     } | ||||
|   } | ||||
|   private initPromise: Promise<void> | null; | ||||
|  |  | |||
|  | @ -61,11 +61,11 @@ function waitForSync(client: MatrixClient) { | |||
|       data: ISyncStateData | ||||
|     ) => { | ||||
|       if (state === "PREPARED") { | ||||
|         client.removeListener(ClientEvent.Sync, onSync); | ||||
|         resolve(); | ||||
|         client.removeListener(ClientEvent.Sync, onSync); | ||||
|       } else if (state === "ERROR") { | ||||
|         reject(data?.error); | ||||
|         client.removeListener(ClientEvent.Sync, onSync); | ||||
|         reject(data?.error); | ||||
|       } | ||||
|     }; | ||||
|     client.on(ClientEvent.Sync, onSync); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue