diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index ccd111b8..80f40d54 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -8,6 +8,7 @@ "{{name}} is talking…": "{{name}} is talking…", "{{names}}, {{name}}": "{{names}}, {{name}}", "{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie call", + "<0>{children}Connectivity to the server has been lost.": "<0>{children}Connectivity to the server has been lost.", "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Already have an account?<1><0>Log in Or <2>Access as a guest", "<0>Create an account Or <2>Access as a guest": "<0>Create an account Or <2>Access as a guest", diff --git a/src/App.tsx b/src/App.tsx index 2ae27b73..655e3789 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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) { + diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 08593a4b..75c1bed3 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -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,7 +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; isAuthenticated: boolean; @@ -81,6 +83,7 @@ interface ClientState { logout: () => void; setClient: (client: MatrixClient, session: Session) => void; error?: Error; + disconnected: boolean; } const ClientContext = createContext(null); @@ -98,7 +101,15 @@ export const ClientProvider: FC = ({ children }) => { const history = useHistory(); const initializing = useRef(false); const [ - { loading, isAuthenticated, isPasswordlessUser, client, userName, error }, + { + loading, + isAuthenticated, + isPasswordlessUser, + client, + userName, + error, + disconnected, + }, setState, ] = useState({ loading: true, @@ -107,8 +118,20 @@ export const ClientProvider: FC = ({ children }) => { client: undefined, userName: null, error: undefined, + disconnected: false, }); + const onSync = (state: SyncState, _old: SyncState, data: ISyncStateData) => { + setState((currentState) => { + logger.log("syntData.name", data?.error?.name, "state:", state); + console.log("Current state:", currentState); + return { + ...currentState, + disconnected: isDisconnected(state, data), + }; + }); + }; + 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 +206,10 @@ export const ClientProvider: FC = ({ children }) => { } } }; - + let clientWithListener; init() .then(({ client, isPasswordlessUser }) => { + clientWithListener = client; setState({ client, loading: false, @@ -193,7 +217,12 @@ export const ClientProvider: FC = ({ 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 +233,13 @@ export const ClientProvider: FC = ({ children }) => { isPasswordlessUser: false, userName: null, error: undefined, + disconnected: false, }); }) .finally(() => (initializing.current = false)); + return () => { + clientWithListener?.removeListener(ClientEvent.Sync, onSync); + }; }, []); const changePassword = useCallback( @@ -235,6 +268,7 @@ export const ClientProvider: FC = ({ children }) => { isPasswordlessUser: false, userName: client.getUserIdLocalpart(), error: undefined, + disconnected: false, }); }, [client] @@ -256,6 +290,10 @@ export const ClientProvider: FC = ({ children }) => { isPasswordlessUser: session.passwordlessUser, userName: newClient.getUserIdLocalpart(), error: undefined, + disconnected: isDisconnected( + newClient.getSyncState(), + newClient.getSyncStateData() + ), }); } else { clearSession(); @@ -267,6 +305,7 @@ export const ClientProvider: FC = ({ children }) => { isPasswordlessUser: false, userName: null, error: undefined, + disconnected: false, }); } }, @@ -284,6 +323,7 @@ export const ClientProvider: FC = ({ children }) => { isPasswordlessUser: true, userName: "", error: undefined, + disconnected: false, }); history.push("/"); PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest); @@ -326,6 +366,7 @@ export const ClientProvider: FC = ({ children }) => { userName, setClient, error: undefined, + disconnected, }), [ loading, @@ -336,6 +377,7 @@ export const ClientProvider: FC = ({ children }) => { logout, userName, setClient, + disconnected, ] ); diff --git a/src/DisconnectedBanner.module.css b/src/DisconnectedBanner.module.css new file mode 100644 index 00000000..aa6f6f5a --- /dev/null +++ b/src/DisconnectedBanner.module.css @@ -0,0 +1,27 @@ +/* +Copyright 2022 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: 100%; +} diff --git a/src/DisconnectedBanner.tsx b/src/DisconnectedBanner.tsx new file mode 100644 index 00000000..84ecc432 --- /dev/null +++ b/src/DisconnectedBanner.tsx @@ -0,0 +1,50 @@ +/* +Copyright 2022 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 { logger } from "@sentry/utils"; +import { Trans } from "react-i18next"; + +import styles from "./DisconnectedBanner.module.css"; +import { useClient } from "./ClientContext"; + +interface DisconnectedBannerProps extends HTMLAttributes { + children?: ReactNode; + className?: string; +} + +export function DisconnectedBanner({ + children, + className, + ...rest +}: DisconnectedBannerProps) { + const clientrp = useClient(); + logger.log(clientrp); + const disconnected = clientrp.disconnected; + return ( + <> + {disconnected && ( + +
+ {children} + Connectivity to the server has been lost. +
+
+ )} + + ); +} diff --git a/src/initializer.tsx b/src/initializer.tsx index 37e659e7..c6507668 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -45,6 +45,12 @@ class DependencyLoadStates { export class Initializer { private static internalInstance: Initializer; + private isInitialized = false; + + public static isInitialized(): boolean { + return Boolean(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 | null; diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 8cec21d0..2830b823 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -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);