diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index d14003dbfac..834801fec16 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -295,9 +295,17 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.threadSupport = true; if (SettingsStore.getValue("feature_sliding_sync")) { - opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); - } else { - SlidingSyncManager.instance.checkSupport(this.matrixClient); + const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); + if (proxyUrl) { + logger.log("Activating sliding sync using proxy at ", proxyUrl); + } else { + logger.log("Activating sliding sync"); + } + opts.slidingSync = SlidingSyncManager.instance.configure( + this.matrixClient, + proxyUrl || this.matrixClient.baseUrl, + ); + SlidingSyncManager.instance.startSpidering(100, 50); // 100 rooms at a time, 50ms apart } // Connect the matrix client to the dispatcher and setting handlers diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index e3f420b43d5..5f459c0b9e6 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -44,7 +44,7 @@ limitations under the License. * list ops) */ -import { MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, EventType } from "matrix-js-sdk/src/matrix"; import { MSC3575Filter, MSC3575List, @@ -56,9 +56,6 @@ import { import { logger } from "matrix-js-sdk/src/logger"; import { defer, sleep } from "matrix-js-sdk/src/utils"; -import SettingsStore from "./settings/SettingsStore"; -import SlidingSyncController from "./settings/controllers/SlidingSyncController"; - // how long to long poll for const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000; @@ -326,93 +323,4 @@ export class SlidingSyncManager { firstTime = false; } } - - /** - * Set up the Sliding Sync instance; configures the end point and starts spidering. - * The sliding sync endpoint is derived the following way: - * 1. The user-defined sliding sync proxy URL (legacy, for backwards compatibility) - * 2. The client `well-known` sliding sync proxy URL [declared at the unstable prefix](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#unstable-prefix) - * 3. The homeserver base url (for native server support) - * @param client The MatrixClient to use - * @returns A working Sliding Sync or undefined - */ - public async setup(client: MatrixClient): Promise { - const baseUrl = client.baseUrl; - const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); - const wellKnownProxyUrl = await this.getProxyFromWellKnown(client); - - const slidingSyncEndpoint = proxyUrl || wellKnownProxyUrl || baseUrl; - - this.configure(client, slidingSyncEndpoint); - logger.info("Sliding sync activated at", slidingSyncEndpoint); - this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart - - return this.slidingSync; - } - - /** - * Get the sliding sync proxy URL from the client well known - * @param client The MatrixClient to use - * @return The proxy url - */ - public async getProxyFromWellKnown(client: MatrixClient): Promise { - let proxyUrl: string | undefined; - - try { - const clientWellKnown = await AutoDiscovery.findClientConfig(client.baseUrl); - proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url; - } catch (e) { - // client.baseUrl is invalid, `AutoDiscovery.findClientConfig` has thrown - } - - if (proxyUrl != undefined) { - logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl); - } - return proxyUrl; - } - - /** - * Check if the server "natively" supports sliding sync (at the unstable endpoint). - * @param client The MatrixClient to use - * @return Whether the "native" (unstable) endpoint is up - */ - public async nativeSlidingSyncSupport(client: MatrixClient): Promise { - try { - await client.http.authedRequest(Method.Post, "/sync", undefined, undefined, { - localTimeoutMs: 10 * 1000, // 10s - prefix: "/_matrix/client/unstable/org.matrix.msc3575", - }); - } catch (e) { - return false; // 404, M_UNRECOGNIZED - } - - logger.log("nativeSlidingSyncSupport: sliding sync endpoint is up"); - return true; // 200, OK - } - - /** - * Check whether our homeserver has sliding sync support, that the endpoint is up, and - * is a sliding sync endpoint. - * - * Sets static member `SlidingSyncController.serverSupportsSlidingSync` - * @param client The MatrixClient to use - */ - public async checkSupport(client: MatrixClient): Promise { - if (await this.nativeSlidingSyncSupport(client)) { - SlidingSyncController.serverSupportsSlidingSync = true; - return; - } - - const proxyUrl = await this.getProxyFromWellKnown(client); - if (proxyUrl != undefined) { - const response = await fetch(proxyUrl + "/client/server.json", { - method: Method.Get, - signal: timeoutSignal(10 * 1000), // 10s - }); - if (response.status === 200) { - logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl); - SlidingSyncController.serverSupportsSlidingSync = true; - } - } - } } diff --git a/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx b/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx new file mode 100644 index 00000000000..958c8d0876d --- /dev/null +++ b/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx @@ -0,0 +1,142 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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 React from "react"; +import { MatrixClient, Method } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { _t } from "../../../languageHandler"; +import SettingsStore from "../../../settings/SettingsStore"; +import TextInputDialog from "./TextInputDialog"; +import withValidation from "../elements/Validation"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; +import { SettingLevel } from "../../../settings/SettingLevel"; + +/** + * Check that the server natively supports sliding sync. + * @param cli The MatrixClient of the logged in user. + * @throws if the proxy server is unreachable or not configured to the given homeserver + */ +async function syncHealthCheck(cli: MatrixClient): Promise { + await cli.http.authedRequest(Method.Post, "/sync", undefined, undefined, { + localTimeoutMs: 10 * 1000, // 10s + prefix: "/_matrix/client/unstable/org.matrix.msc3575", + }); + logger.info("server natively support sliding sync OK"); +} + +/** + * Check that the proxy url is in fact a sliding sync proxy endpoint and it is up. + * @param endpoint The proxy endpoint url + * @param hsUrl The homeserver url of the logged in user. + * @throws if the proxy server is unreachable or not configured to the given homeserver + */ +async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise { + const controller = new AbortController(); + const id = window.setTimeout(() => controller.abort(), 10 * 1000); // 10s + const res = await fetch(endpoint + "/client/server.json", { + signal: controller.signal, + }); + clearTimeout(id); + if (res.status != 200) { + throw new Error(`proxyHealthCheck: proxy server returned HTTP ${res.status}`); + } + const body = await res.json(); + if (body.server !== hsUrl) { + throw new Error(`proxyHealthCheck: client using ${hsUrl} but server is as ${body.server}`); + } + logger.info("sliding sync proxy is OK"); +} + +export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean): void }> = ({ onFinished }) => { + const cli = MatrixClientPeg.safeGet(); + const currentProxy = SettingsStore.getValue("feature_sliding_sync_proxy_url"); + const hasNativeSupport = useAsyncMemo( + () => + syncHealthCheck(cli).then( + () => true, + () => false, + ), + [], + null, + ); + + let nativeSupport: string; + if (hasNativeSupport === null) { + nativeSupport = _t("labs|sliding_sync_checking"); + } else { + nativeSupport = hasNativeSupport + ? _t("labs|sliding_sync_server_support") + : _t("labs|sliding_sync_server_no_support"); + } + + const validProxy = withValidation({ + async deriveData({ value }): Promise<{ error?: unknown }> { + if (!value) return {}; + try { + await proxyHealthCheck(value, MatrixClientPeg.safeGet().baseUrl); + return {}; + } catch (error) { + return { error }; + } + }, + rules: [ + { + key: "required", + test: async ({ value }) => !!value || !!hasNativeSupport, + invalid: () => _t("labs|sliding_sync_server_specify_proxy"), + }, + { + key: "working", + final: true, + test: async (_, { error }) => !error, + valid: () => _t("spotlight|public_rooms|network_dropdown_available_valid"), + invalid: ({ error }) => (error instanceof Error ? error.message : null), + }, + ], + }); + + return ( + +
+ {_t("labs|sliding_sync_disable_warning")} +
+ {nativeSupport} + + } + placeholder={ + hasNativeSupport + ? _t("labs|sliding_sync_proxy_url_optional_label") + : _t("labs|sliding_sync_proxy_url_label") + } + value={currentProxy} + button={_t("action|enable")} + validator={validProxy} + onFinished={(enable, proxyUrl) => { + if (enable) { + SettingsStore.setValue("feature_sliding_sync_proxy_url", null, SettingLevel.DEVICE, proxyUrl); + onFinished(true); + } else { + onFinished(false); + } + }} + /> + ); +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d1f4016aafd..ddd6e3e09c4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1460,9 +1460,16 @@ "rust_crypto_optin_warning": "Switching to the Rust cryptography requires a migration process that may take several minutes. To disable you will need to log out and back in; use with caution!", "rust_crypto_requires_logout": "Once enabled, Rust cryptography can only be disabled by logging out and in again", "sliding_sync": "Sliding Sync mode", + "sliding_sync_checking": "Checking…", + "sliding_sync_configuration": "Sliding Sync configuration", "sliding_sync_description": "Under active development, cannot be disabled.", + "sliding_sync_disable_warning": "To disable you will need to log out and back in, use with caution!", "sliding_sync_disabled_notice": "Log out and back in to disable", - "sliding_sync_server_no_support": "Your server lacks support", + "sliding_sync_proxy_url_label": "Proxy URL", + "sliding_sync_proxy_url_optional_label": "Proxy URL (optional)", + "sliding_sync_server_no_support": "Your server lacks native support", + "sliding_sync_server_specify_proxy": "Your server lacks native support, you must specify a proxy", + "sliding_sync_server_support": "Your server has native support", "under_active_development": "Under active development.", "unrealiable_e2e": "Unreliable in encrypted rooms", "video_rooms": "Video rooms", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 3650e518148..2137837500e 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -407,7 +407,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { controller: new SlidingSyncController(), }, "feature_sliding_sync_proxy_url": { - // This is not a distinct feature, it is a legacy setting for feature_sliding_sync above + // This is not a distinct feature, it is a setting for feature_sliding_sync above supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: "", }, diff --git a/src/settings/controllers/SlidingSyncController.ts b/src/settings/controllers/SlidingSyncController.ts index 7d7ca781288..77bdf7f42f8 100644 --- a/src/settings/controllers/SlidingSyncController.ts +++ b/src/settings/controllers/SlidingSyncController.ts @@ -1,6 +1,5 @@ /* Copyright 2022 The Matrix.org Foundation C.I.C. -Copyright 2024 Ed Geraghty Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,11 +16,18 @@ limitations under the License. import SettingController from "./SettingController"; import PlatformPeg from "../../PlatformPeg"; +import { SettingLevel } from "../SettingLevel"; +import { SlidingSyncOptionsDialog } from "../../components/views/dialogs/SlidingSyncOptionsDialog"; +import Modal from "../../Modal"; import SettingsStore from "../SettingsStore"; import { _t } from "../../languageHandler"; export default class SlidingSyncController extends SettingController { - public static serverSupportsSlidingSync: boolean; + public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise { + const { finished } = Modal.createDialog(SlidingSyncOptionsDialog); + const [value] = await finished; + return newValue === value; // abort the operation if we're already in the state the user chose via modal + } public async onChange(): Promise { PlatformPeg.get()?.reload(); @@ -32,9 +38,6 @@ export default class SlidingSyncController extends SettingController { if (SettingsStore.getValue("feature_sliding_sync")) { return _t("labs|sliding_sync_disabled_notice"); } - if (!SlidingSyncController.serverSupportsSlidingSync) { - return _t("labs|sliding_sync_server_no_support"); - } return false; } diff --git a/test/SlidingSyncManager-test.ts b/test/SlidingSyncManager-test.ts index 757a682d848..76ebd8f15c3 100644 --- a/test/SlidingSyncManager-test.ts +++ b/test/SlidingSyncManager-test.ts @@ -20,8 +20,6 @@ import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { SlidingSyncManager } from "../src/SlidingSyncManager"; import { stubClient } from "./test-utils"; -import SlidingSyncController from "../src/settings/controllers/SlidingSyncController"; -import SettingsStore from "../src/settings/SettingsStore"; jest.mock("matrix-js-sdk/src/sliding-sync"); const MockSlidingSync = >(SlidingSync); @@ -233,53 +231,4 @@ describe("SlidingSyncManager", () => { ); }); }); - describe("checkSupport", () => { - beforeEach(() => { - SlidingSyncController.serverSupportsSlidingSync = false; - jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy"); - }); - it("shorts out if the server has 'native' sliding sync support", async () => { - jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); - await manager.checkSupport(client); - expect(manager.getProxyFromWellKnown).not.toHaveBeenCalled(); // We return earlier - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); - }); - it("tries to find a sliding sync proxy url from the client well-known if there's no 'native' support", async () => { - jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); - await manager.checkSupport(client); - expect(manager.getProxyFromWellKnown).toHaveBeenCalled(); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); - }); - }); - describe("setup", () => { - beforeEach(() => { - jest.spyOn(manager, "configure"); - jest.spyOn(manager, "startSpidering"); - }); - it("uses the baseUrl as a proxy if no proxy is set in the client well-known and the server has no native support", async () => { - await manager.setup(client); - expect(manager.configure).toHaveBeenCalled(); - expect(manager.configure).toHaveBeenCalledWith(client, client.baseUrl); - expect(manager.startSpidering).toHaveBeenCalled(); - }); - it("uses the proxy declared in the client well-known", async () => { - jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy"); - await manager.setup(client); - expect(manager.configure).toHaveBeenCalled(); - expect(manager.configure).toHaveBeenCalledWith(client, "proxy"); - expect(manager.startSpidering).toHaveBeenCalled(); - }); - it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => { - jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy"); - jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { - if (name === "feature_sliding_sync_proxy_url") return "legacy-proxy"; - }); - await manager.setup(client); - expect(manager.configure).toHaveBeenCalled(); - expect(manager.configure).toHaveBeenCalledWith(client, "legacy-proxy"); - expect(manager.startSpidering).toHaveBeenCalled(); - }); - }); });