remove disconnect banner from header (we have the in call reconnect

view)
This commit is contained in:
Timo K.
2026-09-17 20:49:04 +02:00
parent edfdc23aef
commit e791133e74
11 changed files with 15 additions and 255 deletions
+10 -10
View File
@@ -88,7 +88,7 @@ builds the view model with
| Analytics | `src/analytics/PosthogEvents.ts`, `PosthogAnalytics.ts` | `rtcSession.statistics`, account data |
| Types only | `src/UrlParams.ts`, `src/state/MediaDevices.ts`, `AndroidControlledAudioOutput.ts`, `IOSControlledAudioOutput.ts`, `initialMuteState.ts`, `state/media/RingingMediaViewModel.ts` (`RTCCallIntent`), `src/useEvents.ts` (`TypedEventEmitter` types) | replaced by a local `CallIntent` type / kept as generic emitter typing |
| Runtime misc | `src/useLocalStorage.ts` (`TypedEventEmitter`), `src/room/GroupCallErrorBoundary.tsx` (`MatrixError`), `src/room/KnockLobbyView.tsx` (shell) | see S6 |
| Context | `src/ClientContext.tsx` | `useClient`/`useClientState` used by `Avatar`, `sharedKeyManagement`, `useReactionsSender`, `submit-rageshake`, `DisconnectedBanner` |
| Context | `src/ClientContext.tsx` | `useClient`/`useClientState` used by `Avatar`, `sharedKeyManagement`, `useReactionsSender`, `submit-rageshake` |
Hosts: `component/index.tsx:291`, `src/room/useLoadGroupCall.ts:335`,
`sdk/main.ts:128` (own `MatrixRTCSessionManager`; waits on
@@ -433,7 +433,6 @@ MSC4143: sticky member events, slots, the spec key message), in
rageshake requests via `subscribeTimeline`.
- `DeveloperSettingsTab`: sticky probe → `getCapabilities()`; custom LiveKit
URL validation → `driver.getLivekitToken(...)`.
- `DisconnectedBanner` → `HomeserverUnreachable` in `status$` (C12).
- `window.rtcSession` debug handle → `window.matrixRtc = { participation }`.
### 4.5 Hosts
@@ -737,7 +736,7 @@ participation, clientDriver, …)` sits next to it; both build a
`VideoPreview.tsx`, `useRoomInfo()` (replaces `useRoomName/Avatar/JoinRule/State`),
`InviteModal.tsx`, `Avatar.tsx`, `useOwnProfile.ts`, `ProfileSettingsTab.tsx`,
`SettingsModal.tsx`, `DeveloperSettingsTab.tsx`, `submit-rageshake.ts`,
`DisconnectedBanner.tsx`, `analytics/PosthogEvents.ts`, `controls.ts`, and a
`analytics/PosthogEvents.ts`, `controls.ts`, and a
first `CallView.stories.tsx` (lobby, in call, ended) driven by
`MockMatrixDriver`.
- **S4b ☑ (2026-09-16):** `ReactionsSenderProvider` takes `ownIdentifier`,
@@ -785,13 +784,14 @@ roomInfo.encrypted)` (`useRoomEncryptionSystem` keeps the client for the
client driver's public `client`/`room` are gone. **Consequence:** the
component and its dev harness (`ElementCallClientBased`) now run every
call on the crate.
- **Banner on the driver (2026-09-16):** `useHomeserverConnected(drivers,
graceMs)` (`src/driver/`) follows `rtcDriver.isHomeserverConnected()` /
`subscribeConnectivity` and reports a lapse only after
`sync_disconnect_grace_period_ms`, since the driver reports every sync
hiccup where the client state waited for a `ConnectionError`;
`DisconnectedBanner` uses it whenever drivers are provided and falls back
to the client state for the shell. The mock RTC driver keeps several
- **Banner dropped (2026-09-17):** the disconnected banner, its
`useHomeserverConnected` hook and the client state's `disconnected` flag
are gone. The local membership already carries the connection state, so
the reconnecting overlay covers the in-call case where it matters; in the
lobby the banner added little. `HomeserverConnected` in
`state/CallViewModel/localMember/` remains the one consumer of
`rtcDriver.isHomeserverConnected()` / `subscribeConnectivity` and of
`sync_disconnect_grace_period_ms`. The mock RTC driver keeps several
connectivity sinks (the crate's and the UI's).
- **S4 closed (2026-09-16):** `CallView.stories.tsx` (Lobby with a peer,
NoTransport as the error path, Ended) over the mock drivers, no client;
-1
View File
@@ -102,7 +102,6 @@
"show_connection_stats": "Show connection statistics",
"url_params": "URL parameters"
},
"disconnected_banner": "Connectivity to the server has been lost.",
"error": {
"call_is_not_supported": "Call is not supported",
"call_not_found": "Call not found",
-1
View File
@@ -29,7 +29,6 @@ const TestComponent: FC<
<ClientContextProvider
value={{
state: "valid",
disconnected: false,
supportedFeatures: {
reactions: true,
},
+2 -32
View File
@@ -17,8 +17,7 @@ import {
type JSX,
} from "react";
import { logger } from "matrix-js-sdk/lib/logger";
import { type ISyncStateData, type SyncState } from "matrix-js-sdk/lib/sync";
import { ClientEvent, type MatrixClient } from "matrix-js-sdk";
import { type MatrixClient } from "matrix-js-sdk";
import { ErrorPage } from "./FullScreenView";
import { useHostBridge } from "./HostBridge";
@@ -42,9 +41,6 @@ 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;
supportedFeatures: {
reactions: boolean;
};
@@ -276,7 +272,6 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
}, [initClientState?.client, setAlreadyOpenedErr]),
);
const [isDisconnected, setIsDisconnected] = useState(false);
const [supportsReactions, setSupportsReactions] = useState(false);
const state: ClientState | undefined = useMemo(() => {
@@ -300,7 +295,6 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
state: "valid",
authenticated,
setClient,
disconnected: isDisconnected,
supportedFeatures: {
reactions: supportsReactions,
},
@@ -311,17 +305,9 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
initClientState,
logout,
setClient,
isDisconnected,
supportsReactions,
]);
const onSync = useCallback(
(state: SyncState, _old: SyncState | null, data?: ISyncStateData) => {
setIsDisconnected(clientIsDisconnected(state, data));
},
[],
);
useEffect(() => {
if (!initClientState) {
return;
@@ -333,20 +319,10 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
if (PosthogAnalytics.hasInstance())
PosthogAnalytics.instance.onLoginStatusChanged();
if (initClientState.client) {
initClientState.client.on(ClientEvent.Sync, onSync);
}
if (!hostBridge.supportsReactions)
logger.warn("The host does not permit reactions");
setSupportsReactions(hostBridge.supportsReactions);
return (): void => {
if (initClientState.client) {
initClientState.client.removeListener(ClientEvent.Sync, onSync);
}
};
}, [initClientState, onSync, hostBridge]);
}, [initClientState, hostBridge]);
if (alreadyOpenedErr) {
return <ErrorPage error={alreadyOpenedErr} />;
@@ -388,9 +364,3 @@ const loadSession = (): Session | undefined => {
return JSON.parse(data);
};
const clientIsDisconnected = (
syncState: SyncState,
syncData?: ISyncStateData,
): boolean =>
syncState === "ERROR" && syncData?.error?.name === "ConnectionError";
-18
View File
@@ -1,18 +0,0 @@
/*
Copyright 2023, 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
.banner {
position: absolute;
padding: 29px;
background-color: var(--cpd-color-bg-subtle-primary);
vertical-align: middle;
font-size: var(--font-size-body);
text-align: center;
z-index: 1;
top: 76px;
width: calc(100% - 58px);
}
-52
View File
@@ -1,52 +0,0 @@
/*
Copyright 2023, 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import classNames from "classnames";
import { type FC, type HTMLAttributes, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import styles from "./DisconnectedBanner.module.css";
import { type ValidClientState, useClientState } from "./ClientContext";
import { useOptionalMatrixDrivers } from "./driver/MatrixDriverContext";
import { useHomeserverConnected } from "./driver/useHomeserverConnected";
interface Props extends HTMLAttributes<HTMLElement> {
children?: ReactNode;
className?: string;
}
export const DisconnectedBanner: FC<Props> = ({
children,
className,
...rest
}) => {
const { t } = useTranslation();
// Under a call the host's RTC driver says whether the homeserver is
// reachable; the shell outside a call only has the client's sync state.
const drivers = useOptionalMatrixDrivers();
const homeserverConnected = useHomeserverConnected(drivers);
const clientState = useClientState();
let shouldShowBanner = false;
if (drivers !== null) {
shouldShowBanner = !homeserverConnected;
} else if (clientState?.state === "valid") {
const validClientState = clientState as ValidClientState;
shouldShowBanner = validClientState.disconnected;
}
return (
<>
{shouldShowBanner && (
<div className={classNames(styles.banner, className)} {...rest}>
{children}
{t("disconnected_banner")}
</div>
)}
</>
);
};
+3 -18
View File
@@ -23,38 +23,23 @@ import { Avatar, Size } from "./Avatar";
import { EncryptionLock } from "./room/EncryptionLock";
import { useRootSizeMatches } from "./useRootSize";
import { useLeaveToHome } from "./LeaveToHomeContext";
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>
{disconnectedBanner && <DisconnectedBanner />}
</>
<header ref={ref} className={classNames(styles.header, className)} {...rest}>
{children}
</header>
);
};
@@ -1,63 +0,0 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { type MatrixDrivers } from "./MatrixDriverContext";
import { MockElementCallMatrixClientDriver } from "./MockElementCallMatrixClientDriver";
import { MockRtcMatrixDriver } from "./MockRtcMatrixDriver";
import { useHomeserverConnected } from "./useHomeserverConnected";
describe("useHomeserverConnected", () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("reports a lapse only after the grace period, and a return at once", () => {
const rtcDriver = new MockRtcMatrixDriver();
const drivers: MatrixDrivers = {
rtcDriver,
clientDriver: new MockElementCallMatrixClientDriver(),
};
const { result } = renderHook(() => useHomeserverConnected(drivers, 1000));
expect(result.current).toBe(true);
act(() => rtcDriver.setHomeserverConnected(false));
expect(result.current).toBe(true);
act(() => vi.advanceTimersByTime(999));
expect(result.current).toBe(true);
act(() => vi.advanceTimersByTime(1));
expect(result.current).toBe(false);
act(() => rtcDriver.setHomeserverConnected(true));
expect(result.current).toBe(true);
// A blip shorter than the grace period is never shown.
act(() => rtcDriver.setHomeserverConnected(false));
act(() => vi.advanceTimersByTime(500));
act(() => rtcDriver.setHomeserverConnected(true));
act(() => vi.advanceTimersByTime(1000));
expect(result.current).toBe(true);
});
it("starts from the driver's current answer", () => {
const rtcDriver = new MockRtcMatrixDriver();
rtcDriver.setHomeserverConnected(false);
const drivers: MatrixDrivers = {
rtcDriver,
clientDriver: new MockElementCallMatrixClientDriver(),
};
const { result } = renderHook(() => useHomeserverConnected(drivers, 1000));
act(() => vi.advanceTimersByTime(1000));
expect(result.current).toBe(false);
});
it("is connected without drivers", () => {
const { result } = renderHook(() => useHomeserverConnected(null, 1000));
expect(result.current).toBe(true);
});
});
-58
View File
@@ -1,58 +0,0 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { useEffect, useState } from "react";
import { Config } from "../config/Config";
import { type MatrixDrivers } from "./MatrixDriverContext";
/**
* Whether the homeserver is reachable, from the RTC driver's connectivity
* (the same signal the crate turns into `HomeserverUnreachable`). The driver
* reports every lapse of the sync loop; the UI should not, so a lapse counts
* only once it has lasted `graceMs` (the deployment's
* `sync_disconnect_grace_period_ms` by default). Coming back counts at once.
*
* `null` drivers (a host that provided none) read as connected: there is
* nothing to report on.
*/
export function useHomeserverConnected(
drivers: MatrixDrivers | null,
graceMs: number = Config.get().sync_disconnect_grace_period_ms,
): boolean {
const [connected, setConnected] = useState(true);
useEffect(() => {
if (drivers === null) {
setConnected(true);
return;
}
const { rtcDriver } = drivers;
let live = true;
let lapse: ReturnType<typeof setTimeout> | null = null;
const report = (isConnected: boolean): void => {
if (lapse !== null) {
clearTimeout(lapse);
lapse = null;
}
if (isConnected) setConnected(true);
else lapse = setTimeout(() => setConnected(false), graceMs);
};
report(rtcDriver.isHomeserverConnected());
rtcDriver.subscribeConnectivity({
emit: (isConnected) => {
if (!live) return false;
report(isConnected);
return true;
},
});
return (): void => {
live = false;
if (lapse !== null) clearTimeout(lapse);
};
}, [drivers, graceMs]);
return connected;
}
-1
View File
@@ -505,7 +505,6 @@ export const InCallView: FC<InCallViewProps> = ({
[styles.hidden]: !showHeader,
})}
ref={headerRef}
disconnectedBanner={false} // This screen has its own 'reconnecting' toast
>
<LeftNav>
<RoomHeaderInfo
-1
View File
@@ -77,7 +77,6 @@ function renderWithMockClient(
<ClientContextProvider
value={{
state: "valid",
disconnected: false,
supportedFeatures: {
reactions: true,
},