Merge pull request #1293 from vector-im/connectionlostbanner_lk

Connection Lost Banner
This commit is contained in:
David Baker
2023-07-24 09:49:46 +01:00
committed by GitHub
7 changed files with 136 additions and 6 deletions

View File

@@ -25,6 +25,7 @@
"Change layout": "Change layout",
"Close": "Close",
"Confirm password": "Confirm password",
"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",

View File

@@ -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";
const SentryRoute = Sentry.withSentryRouting(Route);
@@ -60,6 +61,7 @@ export default function App({ history }: AppProps) {
<InspectorContextProvider>
<Sentry.ErrorBoundary fallback={errorPage}>
<OverlayProvider>
<DisconnectedBanner />
<Switch>
<SentryRoute exact path="/">
<HomePage />

View File

@@ -25,9 +25,10 @@ import {
useMemo,
} 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 {
@@ -56,6 +57,9 @@ export type ClientState = ValidClientState | ErrorState;
export type ValidClientState = {
state: "valid";
authenticated?: AuthenticatedClient;
// 'Disconnected' rather than 'connected' because it tracks specifically
// whether the client is supposed to be connected but is not
disconnected: boolean;
setClient: (params?: SetClientParams) => void;
};
@@ -264,6 +268,8 @@ export const ClientProvider: FC<Props> = ({ children }) => {
}, [initClientState?.client, setAlreadyOpenedErr, t])
);
const [isDisconnected, setIsDisconnected] = useState(false);
const state: ClientState = useMemo(() => {
if (alreadyOpenedErr) {
return { state: "error", error: alreadyOpenedErr };
@@ -279,8 +285,27 @@ export const ClientProvider: FC<Props> = ({ children }) => {
};
}
return { state: "valid", authenticated, setClient };
}, [alreadyOpenedErr, changePassword, initClientState, logout, setClient]);
return {
state: "valid",
authenticated,
setClient,
disconnected: isDisconnected,
};
}, [
alreadyOpenedErr,
changePassword,
initClientState,
logout,
setClient,
isDisconnected,
]);
const onSync = useCallback(
(state: SyncState, _old: SyncState | null, data?: ISyncStateData) => {
setIsDisconnected(clientIsDisconnected(state, data));
},
[]
);
useEffect(() => {
if (!initClientState) {
@@ -292,7 +317,17 @@ export const ClientProvider: FC<Props> = ({ children }) => {
if (PosthogAnalytics.hasInstance())
PosthogAnalytics.instance.onLoginStatusChanged();
}, [initClientState]);
if (initClientState.client) {
initClientState.client.on(ClientEvent.Sync, onSync);
}
return () => {
if (initClientState.client) {
initClientState.client.removeListener(ClientEvent.Sync, onSync);
}
};
}, [initClientState, onSync]);
if (alreadyOpenedErr) {
return <ErrorView error={alreadyOpenedErr} />;
@@ -387,3 +422,8 @@ const loadSession = (): Session | undefined => {
return JSON.parse(data);
};
const clientIsDisconnected = (
syncState: SyncState,
syncData?: ISyncStateData
) => syncState === "ERROR" && syncData?.error?.name === "ConnectionError";

View 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);
}

View File

@@ -0,0 +1,53 @@
/*
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 { HTMLAttributes, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import styles from "./DisconnectedBanner.module.css";
import { ValidClientState, useClientState } from "./ClientContext";
interface DisconnectedBannerProps extends HTMLAttributes<HTMLElement> {
children?: ReactNode;
className?: string;
}
export function DisconnectedBanner({
children,
className,
...rest
}: DisconnectedBannerProps) {
const { t } = useTranslation();
const clientState = useClientState();
let shouldShowBanner = false;
if (clientState?.state === "valid") {
const validClientState = clientState as ValidClientState;
shouldShowBanner = validClientState.disconnected;
}
return (
<>
{shouldShowBanner && (
<div className={classNames(styles.banner, className)} {...rest}>
{children}
{t("Connectivity to the server has been lost.")}
</div>
)}
</>
);
}

View File

@@ -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
@@ -227,6 +233,7 @@ export class Initializer {
if (this.loadStates.allDepsAreLoaded()) {
// resolve if there is no dependency that is not loaded
resolve();
this.isInitialized = true;
}
}

View File

@@ -60,11 +60,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);