Show 'reconnecting' message when sync loop is disconnected

With this change I'm also taking care to not show the standard "Connection to the server has been lost" banner in the call view, since that is now covered by the 'reconnecting' message.
This commit is contained in:
Robin
2025-08-20 13:30:21 +02:00
parent 8de6ddceb0
commit 1a1e5a9db8
6 changed files with 66 additions and 19 deletions

View File

@@ -24,7 +24,6 @@ import { RegisterPage } from "./auth/RegisterPage";
import { RoomPage } from "./room/RoomPage";
import { ClientProvider } from "./ClientContext";
import { ErrorPage, LoadingPage } from "./FullScreenView";
import { DisconnectedBanner } from "./DisconnectedBanner";
import { Initializer } from "./initializer";
import { widget } from "./widget";
import { useTheme } from "./useTheme";
@@ -86,7 +85,6 @@ export const App: FC<Props> = ({ vm }) => {
<Sentry.ErrorBoundary
fallback={(error) => <ErrorPage error={error} widget={widget} />}
>
<DisconnectedBanner />
<Routes>
<SentryRoute path="/" element={<HomePage />} />
<SentryRoute path="/login" element={<LoginPage />} />

View File

@@ -61,7 +61,11 @@ export const AppBar: FC<Props> = ({ children }) => {
style={{ display: hidden ? "none" : "block" }}
className={styles.bar}
>
<Header>
<Header
// App bar is mainly seen in the call view, which has its own
// 'reconnecting' toast
disconnectedBanner={false}
>
<LeftNav>
<Tooltip label={t("common.back")}>
<IconButton onClick={onBackClick}>

View File

@@ -17,27 +17,38 @@ import Logo from "./icons/Logo.svg?react";
import { Avatar, Size } from "./Avatar";
import { EncryptionLock } from "./room/EncryptionLock";
import { useMediaQuery } from "./useMediaQuery";
import { DisconnectedBanner } from "./DisconnectedBanner";
interface HeaderProps extends HTMLAttributes<HTMLElement> {
ref?: Ref<HTMLElement>;
children: ReactNode;
className?: string;
/**
* Whether the header should display an informational banner whenever the
* client is disconnected from the homeserver.
* @default true
*/
disconnectedBanner?: boolean;
}
export const Header: FC<HeaderProps> = ({
ref,
children,
className,
disconnectedBanner = true,
...rest
}) => {
return (
<header
ref={ref}
className={classNames(styles.header, className)}
{...rest}
>
{children}
</header>
<>
<header
ref={ref}
className={classNames(styles.header, className)}
{...rest}
>
{children}
</header>
{disconnectedBanner && <DisconnectedBanner />}
</>
);
};

View File

@@ -508,7 +508,11 @@ export const InCallView: FC<InCallViewProps> = ({
break;
case "standard":
header = (
<Header className={styles.header} ref={headerRef}>
<Header
className={styles.header}
ref={headerRef}
disconnectedBanner={false} // This screen has its own 'reconnecting' toast
>
<LeftNav>
<RoomHeaderInfo
id={matrixInfo.roomId}

View File

@@ -19,7 +19,9 @@ import {
Track,
} from "livekit-client";
import {
ClientEvent,
RoomStateEvent,
SyncState,
type Room as MatrixRoom,
type RoomMember,
} from "matrix-js-sdk";
@@ -69,7 +71,7 @@ import {
ScreenShareViewModel,
type UserMediaViewModel,
} from "./MediaViewModel";
import { accumulate, finalizeValue } from "../utils/observable";
import { accumulate, and$, finalizeValue } from "../utils/observable";
import { ObservableScope } from "./ObservableScope";
import {
duplicateTiles,
@@ -494,18 +496,37 @@ export class CallViewModel extends ViewModel {
map(() => this.matrixRTCSession.memberships),
);
private readonly matrixRTCConnected$ = this.scope.behavior(
this.memberships$.pipe(
map((ms) =>
ms.some(
(m) => m.sender === this.userId && m.deviceId === this.deviceId,
private readonly matrixConnected$ = this.scope.behavior(
// To consider ourselves connected to MatrixRTC, we check the following:
and$(
// The client is connected to the sync loop
(
fromEvent(this.matrixRoom.client, ClientEvent.Sync) as Observable<
[SyncState]
>
).pipe(
startWith([this.matrixRoom.client.getSyncState()]),
map(([state]) => state === SyncState.Syncing),
),
// We can see our own call membership
this.memberships$.pipe(
map((ms) =>
ms.some(
(m) => m.sender === this.userId && m.deviceId === this.deviceId,
),
),
),
),
);
// TODO: Account for LiveKit connection state too
private readonly connected$ = this.matrixConnected$;
/**
* Whether we should tell the user that we're reconnecting to the call.
*/
public readonly reconnecting$ = this.scope.behavior(
this.matrixRTCConnected$.pipe(
this.connected$.pipe(
// We are reconnecting if we previously had some successful initial
// connection but are now disconnected
scan(
@@ -1533,7 +1554,7 @@ export class CallViewModel extends ViewModel {
// Pause all media tracks when we're disconnected from MatrixRTC, because it
// can be an unpleasant surprise for the app to say 'reconnecting' and yet
// still be transmitting your media to others.
this.matrixRTCConnected$.pipe(this.scope.bind()).subscribe((connected) => {
this.matrixConnected$.pipe(this.scope.bind()).subscribe((connected) => {
const publications =
this.livekitRoom.localParticipant.trackPublications.values();
if (connected) {

View File

@@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details.
import {
type Observable,
combineLatest,
concat,
defer,
finalize,
@@ -86,3 +87,11 @@ export function getValue<T>(state$: Observable<T>): T {
if (value === nothing) throw new Error("Not a state Observable");
return value;
}
/**
* Creates an Observable that has a value of true whenever all its inputs are
* true.
*/
export function and$(...inputs: Observable<boolean>[]): Observable<boolean> {
return combineLatest(inputs, (...flags) => flags.every((flag) => flag));
}