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",
|
"Close": "Close",
|
||||||
"Confirm password": "Confirm password",
|
"Confirm password": "Confirm password",
|
||||||
"Connection lost": "Connection lost",
|
"Connection lost": "Connection lost",
|
||||||
|
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||||
"Copied!": "Copied!",
|
"Copied!": "Copied!",
|
||||||
"Copy": "Copy",
|
"Copy": "Copy",
|
||||||
"Copy and share this call link": "Copy and share this call link",
|
"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 { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||||
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||||
import { CrashView, LoadingView } from "./FullScreenView";
|
import { CrashView, LoadingView } from "./FullScreenView";
|
||||||
|
import { DisconnectedBanner } from "./DisconnectedBanner";
|
||||||
import { Initializer } from "./initializer";
|
import { Initializer } from "./initializer";
|
||||||
import { MediaHandlerProvider } from "./settings/useMediaHandler";
|
import { MediaHandlerProvider } from "./settings/useMediaHandler";
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ export default function App({ history }: AppProps) {
|
||||||
<InspectorContextProvider>
|
<InspectorContextProvider>
|
||||||
<Sentry.ErrorBoundary fallback={errorPage}>
|
<Sentry.ErrorBoundary fallback={errorPage}>
|
||||||
<OverlayProvider>
|
<OverlayProvider>
|
||||||
|
<DisconnectedBanner />
|
||||||
<Switch>
|
<Switch>
|
||||||
<SentryRoute exact path="/">
|
<SentryRoute exact path="/">
|
||||||
<HomePage />
|
<HomePage />
|
||||||
|
|
|
@ -25,9 +25,10 @@ import React, {
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||||
|
|
||||||
import { ErrorView } from "./FullScreenView";
|
import { ErrorView } from "./FullScreenView";
|
||||||
import {
|
import {
|
||||||
|
@ -70,6 +71,8 @@ const loadSession = (): Session => {
|
||||||
const saveSession = (session: Session) =>
|
const saveSession = (session: Session) =>
|
||||||
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
||||||
const clearSession = () => localStorage.removeItem("matrix-auth-store");
|
const clearSession = () => localStorage.removeItem("matrix-auth-store");
|
||||||
|
const isDisconnected = (syncState, syncData) =>
|
||||||
|
syncState === "ERROR" && syncData?.error?.name === "ConnectionError";
|
||||||
|
|
||||||
interface ClientState {
|
interface ClientState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -81,6 +84,7 @@ interface ClientState {
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
setClient: (client: MatrixClient, session: Session) => void;
|
setClient: (client: MatrixClient, session: Session) => void;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
|
disconnected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientContext = createContext<ClientState>(null);
|
const ClientContext = createContext<ClientState>(null);
|
||||||
|
@ -98,7 +102,15 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const initializing = useRef(false);
|
const initializing = useRef(false);
|
||||||
const [
|
const [
|
||||||
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
{
|
||||||
|
loading,
|
||||||
|
isAuthenticated,
|
||||||
|
isPasswordlessUser,
|
||||||
|
client,
|
||||||
|
userName,
|
||||||
|
error,
|
||||||
|
disconnected,
|
||||||
|
},
|
||||||
setState,
|
setState,
|
||||||
] = useState<ClientProviderState>({
|
] = useState<ClientProviderState>({
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -107,8 +119,18 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
client: undefined,
|
client: undefined,
|
||||||
userName: null,
|
userName: null,
|
||||||
error: undefined,
|
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(() => {
|
useEffect(() => {
|
||||||
// In case the component is mounted, unmounted, and remounted quickly (as
|
// 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
|
// 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()
|
init()
|
||||||
.then(({ client, isPasswordlessUser }) => {
|
.then(({ client, isPasswordlessUser }) => {
|
||||||
|
clientWithListener = client;
|
||||||
setState({
|
setState({
|
||||||
client,
|
client,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -193,7 +216,12 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
userName: client?.getUserIdLocalpart(),
|
userName: client?.getUserIdLocalpart(),
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
disconnected: isDisconnected(
|
||||||
|
client?.getSyncState,
|
||||||
|
client?.getSyncStateData
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
clientWithListener?.on(ClientEvent.Sync, onSync);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
@ -204,9 +232,13 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
isPasswordlessUser: false,
|
isPasswordlessUser: false,
|
||||||
userName: null,
|
userName: null,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
disconnected: false,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => (initializing.current = false));
|
.finally(() => (initializing.current = false));
|
||||||
|
return () => {
|
||||||
|
clientWithListener?.removeListener(ClientEvent.Sync, onSync);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const changePassword = useCallback(
|
const changePassword = useCallback(
|
||||||
|
@ -235,6 +267,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
isPasswordlessUser: false,
|
isPasswordlessUser: false,
|
||||||
userName: client.getUserIdLocalpart(),
|
userName: client.getUserIdLocalpart(),
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
disconnected: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[client]
|
[client]
|
||||||
|
@ -256,6 +289,10 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
isPasswordlessUser: session.passwordlessUser,
|
isPasswordlessUser: session.passwordlessUser,
|
||||||
userName: newClient.getUserIdLocalpart(),
|
userName: newClient.getUserIdLocalpart(),
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
disconnected: isDisconnected(
|
||||||
|
newClient.getSyncState(),
|
||||||
|
newClient.getSyncStateData()
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
clearSession();
|
clearSession();
|
||||||
|
@ -267,6 +304,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
isPasswordlessUser: false,
|
isPasswordlessUser: false,
|
||||||
userName: null,
|
userName: null,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
disconnected: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -284,6 +322,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
isPasswordlessUser: true,
|
isPasswordlessUser: true,
|
||||||
userName: "",
|
userName: "",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
disconnected: false,
|
||||||
});
|
});
|
||||||
history.push("/");
|
history.push("/");
|
||||||
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
|
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
|
||||||
|
@ -326,6 +365,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
userName,
|
userName,
|
||||||
setClient,
|
setClient,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
disconnected,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
loading,
|
loading,
|
||||||
|
@ -336,6 +376,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
logout,
|
logout,
|
||||||
userName,
|
userName,
|
||||||
setClient,
|
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 {
|
export class Initializer {
|
||||||
private static internalInstance: Initializer;
|
private static internalInstance: Initializer;
|
||||||
|
private isInitialized = false;
|
||||||
|
|
||||||
|
public static isInitialized(): boolean {
|
||||||
|
return Initializer.internalInstance?.isInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
public static initBeforeReact() {
|
public static initBeforeReact() {
|
||||||
// this maybe also needs to return a promise in the future,
|
// this maybe also needs to return a promise in the future,
|
||||||
// if we have to do async inits before showing the loading screen
|
// if we have to do async inits before showing the loading screen
|
||||||
|
@ -223,6 +229,7 @@ export class Initializer {
|
||||||
if (this.loadStates.allDepsAreLoaded()) {
|
if (this.loadStates.allDepsAreLoaded()) {
|
||||||
// resolve if there is no dependency that is not loaded
|
// resolve if there is no dependency that is not loaded
|
||||||
resolve();
|
resolve();
|
||||||
|
this.isInitialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private initPromise: Promise<void> | null;
|
private initPromise: Promise<void> | null;
|
||||||
|
|
|
@ -61,11 +61,11 @@ function waitForSync(client: MatrixClient) {
|
||||||
data: ISyncStateData
|
data: ISyncStateData
|
||||||
) => {
|
) => {
|
||||||
if (state === "PREPARED") {
|
if (state === "PREPARED") {
|
||||||
|
client.removeListener(ClientEvent.Sync, onSync);
|
||||||
resolve();
|
resolve();
|
||||||
client.removeListener(ClientEvent.Sync, onSync);
|
|
||||||
} else if (state === "ERROR") {
|
} else if (state === "ERROR") {
|
||||||
reject(data?.error);
|
|
||||||
client.removeListener(ClientEvent.Sync, onSync);
|
client.removeListener(ClientEvent.Sync, onSync);
|
||||||
|
reject(data?.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
client.on(ClientEvent.Sync, onSync);
|
client.on(ClientEvent.Sync, onSync);
|
||||||
|
|
Loading…
Add table
Reference in a new issue