From e791133e741957ee131e89b7e9ab847832c2f03d Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Thu, 17 Sep 2026 20:49:04 +0200 Subject: [PATCH] remove disconnect banner from header (we have the in call reconnect view) --- element-call-oxidation-plan.md | 20 +++---- locales/en/app.json | 1 - src/Avatar.test.tsx | 1 - src/ClientContext.tsx | 34 +----------- src/DisconnectedBanner.module.css | 18 ------- src/DisconnectedBanner.tsx | 52 ------------------ src/Header.tsx | 21 ++------ src/driver/useHomeserverConnected.test.tsx | 63 ---------------------- src/driver/useHomeserverConnected.ts | 58 -------------------- src/room/InCallView.tsx | 1 - src/settings/useSubmitRageshake.test.tsx | 1 - 11 files changed, 15 insertions(+), 255 deletions(-) delete mode 100644 src/DisconnectedBanner.module.css delete mode 100644 src/DisconnectedBanner.tsx delete mode 100644 src/driver/useHomeserverConnected.test.tsx delete mode 100644 src/driver/useHomeserverConnected.ts diff --git a/element-call-oxidation-plan.md b/element-call-oxidation-plan.md index 38a541a8f..11a19b97e 100644 --- a/element-call-oxidation-plan.md +++ b/element-call-oxidation-plan.md @@ -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; diff --git a/locales/en/app.json b/locales/en/app.json index 6b9909d40..7da70a03f 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -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", diff --git a/src/Avatar.test.tsx b/src/Avatar.test.tsx index c5d5e25af..ee47872c9 100644 --- a/src/Avatar.test.tsx +++ b/src/Avatar.test.tsx @@ -29,7 +29,6 @@ const TestComponent: FC< = ({ 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 = ({ children, client }) => { state: "valid", authenticated, setClient, - disconnected: isDisconnected, supportedFeatures: { reactions: supportsReactions, }, @@ -311,17 +305,9 @@ export const ClientProvider: FC = ({ 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 = ({ 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 ; @@ -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"; diff --git a/src/DisconnectedBanner.module.css b/src/DisconnectedBanner.module.css deleted file mode 100644 index c15658a23..000000000 --- a/src/DisconnectedBanner.module.css +++ /dev/null @@ -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); -} diff --git a/src/DisconnectedBanner.tsx b/src/DisconnectedBanner.tsx deleted file mode 100644 index 42c617ec6..000000000 --- a/src/DisconnectedBanner.tsx +++ /dev/null @@ -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 { - children?: ReactNode; - className?: string; -} - -export const DisconnectedBanner: FC = ({ - 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 && ( -
- {children} - {t("disconnected_banner")} -
- )} - - ); -}; diff --git a/src/Header.tsx b/src/Header.tsx index e3bda1659..1965e5e5d 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -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 { ref?: Ref; 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 = ({ ref, children, className, - disconnectedBanner = true, ...rest }) => { return ( - <> -
- {children} -
- {disconnectedBanner && } - +
+ {children} +
); }; diff --git a/src/driver/useHomeserverConnected.test.tsx b/src/driver/useHomeserverConnected.test.tsx deleted file mode 100644 index c641cfd61..000000000 --- a/src/driver/useHomeserverConnected.test.tsx +++ /dev/null @@ -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); - }); -}); diff --git a/src/driver/useHomeserverConnected.ts b/src/driver/useHomeserverConnected.ts deleted file mode 100644 index e4747f898..000000000 --- a/src/driver/useHomeserverConnected.ts +++ /dev/null @@ -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 | 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; -} diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 7efb8db3d..c311c1749 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -505,7 +505,6 @@ export const InCallView: FC = ({ [styles.hidden]: !showHeader, })} ref={headerRef} - disconnectedBanner={false} // This screen has its own 'reconnecting' toast >