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 | | 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 | | 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 | | 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`, Hosts: `component/index.tsx:291`, `src/room/useLoadGroupCall.ts:335`,
`sdk/main.ts:128` (own `MatrixRTCSessionManager`; waits on `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`. rageshake requests via `subscribeTimeline`.
- `DeveloperSettingsTab`: sticky probe → `getCapabilities()`; custom LiveKit - `DeveloperSettingsTab`: sticky probe → `getCapabilities()`; custom LiveKit
URL validation → `driver.getLivekitToken(...)`. URL validation → `driver.getLivekitToken(...)`.
- `DisconnectedBanner` → `HomeserverUnreachable` in `status$` (C12).
- `window.rtcSession` debug handle → `window.matrixRtc = { participation }`. - `window.rtcSession` debug handle → `window.matrixRtc = { participation }`.
### 4.5 Hosts ### 4.5 Hosts
@@ -737,7 +736,7 @@ participation, clientDriver, …)` sits next to it; both build a
`VideoPreview.tsx`, `useRoomInfo()` (replaces `useRoomName/Avatar/JoinRule/State`), `VideoPreview.tsx`, `useRoomInfo()` (replaces `useRoomName/Avatar/JoinRule/State`),
`InviteModal.tsx`, `Avatar.tsx`, `useOwnProfile.ts`, `ProfileSettingsTab.tsx`, `InviteModal.tsx`, `Avatar.tsx`, `useOwnProfile.ts`, `ProfileSettingsTab.tsx`,
`SettingsModal.tsx`, `DeveloperSettingsTab.tsx`, `submit-rageshake.ts`, `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 first `CallView.stories.tsx` (lobby, in call, ended) driven by
`MockMatrixDriver`. `MockMatrixDriver`.
- **S4b ☑ (2026-09-16):** `ReactionsSenderProvider` takes `ownIdentifier`, - **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 client driver's public `client`/`room` are gone. **Consequence:** the
component and its dev harness (`ElementCallClientBased`) now run every component and its dev harness (`ElementCallClientBased`) now run every
call on the crate. call on the crate.
- **Banner on the driver (2026-09-16):** `useHomeserverConnected(drivers, - **Banner dropped (2026-09-17):** the disconnected banner, its
graceMs)` (`src/driver/`) follows `rtcDriver.isHomeserverConnected()` / `useHomeserverConnected` hook and the client state's `disconnected` flag
`subscribeConnectivity` and reports a lapse only after are gone. The local membership already carries the connection state, so
`sync_disconnect_grace_period_ms`, since the driver reports every sync the reconnecting overlay covers the in-call case where it matters; in the
hiccup where the client state waited for a `ConnectionError`; lobby the banner added little. `HomeserverConnected` in
`DisconnectedBanner` uses it whenever drivers are provided and falls back `state/CallViewModel/localMember/` remains the one consumer of
to the client state for the shell. The mock RTC driver keeps several `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). connectivity sinks (the crate's and the UI's).
- **S4 closed (2026-09-16):** `CallView.stories.tsx` (Lobby with a peer, - **S4 closed (2026-09-16):** `CallView.stories.tsx` (Lobby with a peer,
NoTransport as the error path, Ended) over the mock drivers, no client; 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", "show_connection_stats": "Show connection statistics",
"url_params": "URL parameters" "url_params": "URL parameters"
}, },
"disconnected_banner": "Connectivity to the server has been lost.",
"error": { "error": {
"call_is_not_supported": "Call is not supported", "call_is_not_supported": "Call is not supported",
"call_not_found": "Call not found", "call_not_found": "Call not found",
-1
View File
@@ -29,7 +29,6 @@ const TestComponent: FC<
<ClientContextProvider <ClientContextProvider
value={{ value={{
state: "valid", state: "valid",
disconnected: false,
supportedFeatures: { supportedFeatures: {
reactions: true, reactions: true,
}, },
+2 -32
View File
@@ -17,8 +17,7 @@ import {
type JSX, type JSX,
} from "react"; } from "react";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { type ISyncStateData, type SyncState } from "matrix-js-sdk/lib/sync"; import { type MatrixClient } from "matrix-js-sdk";
import { ClientEvent, type MatrixClient } from "matrix-js-sdk";
import { ErrorPage } from "./FullScreenView"; import { ErrorPage } from "./FullScreenView";
import { useHostBridge } from "./HostBridge"; import { useHostBridge } from "./HostBridge";
@@ -42,9 +41,6 @@ export type ClientState = ValidClientState | ErrorState;
export type ValidClientState = { export type ValidClientState = {
state: "valid"; state: "valid";
authenticated?: AuthenticatedClient; authenticated?: AuthenticatedClient;
// 'Disconnected' rather than 'connected' because it tracks specifically
// whether the client is supposed to be connected but is not
disconnected: boolean;
supportedFeatures: { supportedFeatures: {
reactions: boolean; reactions: boolean;
}; };
@@ -276,7 +272,6 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
}, [initClientState?.client, setAlreadyOpenedErr]), }, [initClientState?.client, setAlreadyOpenedErr]),
); );
const [isDisconnected, setIsDisconnected] = useState(false);
const [supportsReactions, setSupportsReactions] = useState(false); const [supportsReactions, setSupportsReactions] = useState(false);
const state: ClientState | undefined = useMemo(() => { const state: ClientState | undefined = useMemo(() => {
@@ -300,7 +295,6 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
state: "valid", state: "valid",
authenticated, authenticated,
setClient, setClient,
disconnected: isDisconnected,
supportedFeatures: { supportedFeatures: {
reactions: supportsReactions, reactions: supportsReactions,
}, },
@@ -311,17 +305,9 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
initClientState, initClientState,
logout, logout,
setClient, setClient,
isDisconnected,
supportsReactions, supportsReactions,
]); ]);
const onSync = useCallback(
(state: SyncState, _old: SyncState | null, data?: ISyncStateData) => {
setIsDisconnected(clientIsDisconnected(state, data));
},
[],
);
useEffect(() => { useEffect(() => {
if (!initClientState) { if (!initClientState) {
return; return;
@@ -333,20 +319,10 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
if (PosthogAnalytics.hasInstance()) if (PosthogAnalytics.hasInstance())
PosthogAnalytics.instance.onLoginStatusChanged(); PosthogAnalytics.instance.onLoginStatusChanged();
if (initClientState.client) {
initClientState.client.on(ClientEvent.Sync, onSync);
}
if (!hostBridge.supportsReactions) if (!hostBridge.supportsReactions)
logger.warn("The host does not permit reactions"); logger.warn("The host does not permit reactions");
setSupportsReactions(hostBridge.supportsReactions); setSupportsReactions(hostBridge.supportsReactions);
}, [initClientState, hostBridge]);
return (): void => {
if (initClientState.client) {
initClientState.client.removeListener(ClientEvent.Sync, onSync);
}
};
}, [initClientState, onSync, hostBridge]);
if (alreadyOpenedErr) { if (alreadyOpenedErr) {
return <ErrorPage error={alreadyOpenedErr} />; return <ErrorPage error={alreadyOpenedErr} />;
@@ -388,9 +364,3 @@ const loadSession = (): Session | undefined => {
return JSON.parse(data); 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>
)}
</>
);
};
+1 -16
View File
@@ -23,38 +23,23 @@ import { Avatar, Size } from "./Avatar";
import { EncryptionLock } from "./room/EncryptionLock"; import { EncryptionLock } from "./room/EncryptionLock";
import { useRootSizeMatches } from "./useRootSize"; import { useRootSizeMatches } from "./useRootSize";
import { useLeaveToHome } from "./LeaveToHomeContext"; import { useLeaveToHome } from "./LeaveToHomeContext";
import { DisconnectedBanner } from "./DisconnectedBanner";
interface HeaderProps extends HTMLAttributes<HTMLElement> { interface HeaderProps extends HTMLAttributes<HTMLElement> {
ref?: Ref<HTMLElement>; ref?: Ref<HTMLElement>;
children: ReactNode; children: ReactNode;
className?: string; 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> = ({ export const Header: FC<HeaderProps> = ({
ref, ref,
children, children,
className, className,
disconnectedBanner = true,
...rest ...rest
}) => { }) => {
return ( return (
<> <header ref={ref} className={classNames(styles.header, className)} {...rest}>
<header
ref={ref}
className={classNames(styles.header, className)}
{...rest}
>
{children} {children}
</header> </header>
{disconnectedBanner && <DisconnectedBanner />}
</>
); );
}; };
@@ -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, [styles.hidden]: !showHeader,
})} })}
ref={headerRef} ref={headerRef}
disconnectedBanner={false} // This screen has its own 'reconnecting' toast
> >
<LeftNav> <LeftNav>
<RoomHeaderInfo <RoomHeaderInfo
-1
View File
@@ -77,7 +77,6 @@ function renderWithMockClient(
<ClientContextProvider <ClientContextProvider
value={{ value={{
state: "valid", state: "valid",
disconnected: false,
supportedFeatures: { supportedFeatures: {
reactions: true, reactions: true,
}, },