diff --git a/locales/en/app.json b/locales/en/app.json index e31c7d50a..5be1bfff6 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -59,10 +59,10 @@ "developer_mode": { "always_show_iphone_earpiece": "Show iPhone earpiece option on all platforms", "crypto_version": "Crypto version: {{version}}", - "custom_livekit_url": { - "current_url": "Currently set to: ", + "custom_transport": { + "current": "Currently set to: ", "from_config": "Currently, no overwrite is set. Url from config is used.", - "label": "Custom Livekit-url", + "label": "Custom transport JSON", "reset": "Reset overwrite", "save": "Save", "saving": "Saving..." diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 7f35c3887..bc0d68c4e 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -694,18 +694,19 @@ export const InCallView: FC = ({ tab={settingsTab} onTabChange={setSettingsTab} developerSettingsVm={developerSettingsVm} - livekitRooms={allConnections.map((connectionItem) => ({ - room: connectionItem.livekitRoom, - livekitAlias: connectionItem.livekitAlias, - // TODO compute is local or tag it in the livekit room items already - isLocal: undefined, - url: - // It should be relatively clear to the user which kind of URL - // this is, based on the scheme (ws:// vs. http://) - "url" in connectionItem.transport - ? connectionItem.transport.url - : connectionItem.transport.livekit_service_url, - }))} + livekitRooms={allConnections.map( + ({ livekitRoom: room, livekitAlias, transport, role }) => ({ + room, + livekitAlias, + role, + url: + // It should be relatively clear to the user which kind of URL + // this is, based on the scheme (ws:// vs. http://) + "url" in transport + ? transport.url + : transport.livekit_service_url, + }), + )} /> )} diff --git a/src/settings/DeveloperSettingsTab.test.tsx b/src/settings/DeveloperSettingsTab.test.tsx index 034249ced..82764d8ec 100644 --- a/src/settings/DeveloperSettingsTab.test.tsx +++ b/src/settings/DeveloperSettingsTab.test.tsx @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { afterEach, describe, expect, it, type Mock, vi } from "vitest"; import { render, waitFor, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; +import userEvent, { type UserEvent } from "@testing-library/user-event"; import { TooltipProvider } from "@vector-im/compound-web"; import type { MatrixClient } from "matrix-js-sdk"; @@ -19,12 +19,12 @@ import { import { outOfCallDeveloperSettingsTabViewModel } from "./DeveloperSettingsTabViewModel"; import { createStaticViewModel } from "../state/ViewModel"; import { - customLivekitUrl as customLivekitUrlSetting, + customTransport as customTransportSetting, enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting, matrixRTCMode as matrixRTCModeSetting, } from "./settings"; import { MatrixRTCMode } from "../config/ConfigOptions"; -import { mockConfig } from "../utils/test"; +import { exampleTransport, mockConfig } from "../utils/test"; import { getSFUConfig } from "../livekit/auth"; // Mock url params hook to avoid environment-dependent snapshot churn. @@ -77,6 +77,21 @@ function createMockMatrixClient(): MatrixClient { } as unknown as MatrixClient; } +/** + * Types the JSON representation of an object into the given element. + */ +async function typeJson( + user: UserEvent, + element: Element, + object: object, +): Promise { + // `user.type` requires us to escape brackets by doubling them + await user.type( + element, + JSON.stringify(object).replace(/([{[\]])/g, (c) => c + c), + ); +} + describe("DeveloperSettingsTab", () => { it("renders and matches snapshot", async () => { const client = createMockMatrixClient(); @@ -124,9 +139,9 @@ describe("DeveloperSettingsTab", () => { expect(container).toMatchSnapshot(); }); - describe("custom livekit url", () => { + describe("custom transport", () => { afterEach(() => { - customLivekitUrlSetting.setValue(null); + customTransportSetting.setValue(null); }); const client = { doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(true), @@ -135,7 +150,7 @@ describe("DeveloperSettingsTab", () => { getDeviceId: () => "DEVICE", getDomain: () => "hs", } as unknown as MatrixClient; - it("will not update custom livekit url without roomId", async () => { + it("will not update custom transport without roomId", async () => { const user = userEvent.setup(); render( @@ -148,17 +163,17 @@ describe("DeveloperSettingsTab", () => { , ); - const input = screen.getByLabelText("Custom Livekit-url"); + const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); - await user.type(input, "wss://example.livekit.invalid"); + await typeJson(user, input, exampleTransport); const saveButton = screen.getByRole("button", { name: "Save" }); await user.click(saveButton); expect(getSFUConfig).not.toHaveBeenCalled(); - expect(customLivekitUrlSetting.getValue()).toBe(null); + expect(customTransportSetting.getValue()).toBe(null); }); - it("will not update custom livekit url without text in input", async () => { + it("will not update custom transport without text in input", async () => { const user = userEvent.setup(); render( @@ -172,16 +187,16 @@ describe("DeveloperSettingsTab", () => { , ); - const input = screen.getByLabelText("Custom Livekit-url"); + const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); const saveButton = screen.getByRole("button", { name: "Save" }); await user.click(saveButton); expect(getSFUConfig).not.toHaveBeenCalled(); - expect(customLivekitUrlSetting.getValue()).toBe(null); + expect(customTransportSetting.getValue()).toBe(null); }); - it("will not update custom livekit url when pressing cancel", async () => { + it("will not update custom transport when pressing cancel", async () => { const user = userEvent.setup(); render( @@ -195,7 +210,7 @@ describe("DeveloperSettingsTab", () => { , ); - const input = screen.getByLabelText("Custom Livekit-url"); + const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); await user.type(input, "wss://example.livekit.invalid"); @@ -205,9 +220,9 @@ describe("DeveloperSettingsTab", () => { await user.click(cancelButton); expect(getSFUConfig).not.toHaveBeenCalled(); - expect(customLivekitUrlSetting.getValue()).toBe(null); + expect(customTransportSetting.getValue()).toBe(null); }); - it("will update custom livekit url", async () => { + it("will update custom transport", async () => { const user = userEvent.setup(); render( @@ -221,27 +236,28 @@ describe("DeveloperSettingsTab", () => { , ); - const input = screen.getByLabelText("Custom Livekit-url"); + const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); - await user.type(input, "wss://example.livekit.valid"); + await typeJson(user, input, { + type: "livekit", + url: "wss://example.livekit.valid", + }); const saveButton = screen.getByRole("button", { name: "Save" }); await user.click(saveButton); expect(getSFUConfig).toHaveBeenCalledWith( expect.objectContaining({ - transport: { - type: "livekit", - livekit_service_url: "wss://example.livekit.valid", - }, + transport: { type: "livekit", url: "wss://example.livekit.valid" }, roomId: "#testRoom", }), ); - expect(customLivekitUrlSetting.getValue()).toBe( - "wss://example.livekit.valid", - ); + expect(customTransportSetting.getValue()).toEqual({ + type: "livekit", + url: "wss://example.livekit.valid", + }); }); - it("will show error on invalid url", async () => { + it("will show error on unreachable transport", async () => { const user = userEvent.setup(); render( @@ -255,24 +271,27 @@ describe("DeveloperSettingsTab", () => { , ); - const input = screen.getByLabelText("Custom Livekit-url"); + const input = screen.getByLabelText("Custom transport JSON"); await user.clear(input); - await user.type(input, "wss://example.livekit.valid"); + await typeJson(user, input, { + type: "livekit", + url: "wss://example.livekit.valid", + }); const saveButton = screen.getByRole("button", { name: "Save" }); (getSFUConfig as Mock).mockImplementation(() => { - throw new Error("Invalid URL"); + throw new Error("Failed to get SFU config"); }); await user.click(saveButton); expect( - screen.getByText("invalid URL (did not update)"), + screen.getByText("invalid transport (did not update)"), ).toBeInTheDocument(); - expect(customLivekitUrlSetting.getValue()).toBe(null); + expect(customTransportSetting.getValue()).toBe(null); }); }); // Add this test inside the describe("DeveloperSettingsTab", () => { block, - // after the custom livekit url tests: + // after the custom transport tests: describe("enable extended livekit logs", () => { afterEach(() => { diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index d7a9164c5..3ef943ddf 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -45,7 +45,7 @@ import { muteAllAudio as muteAllAudioSetting, alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, matrixRTCMode as matrixRTCModeSetting, - customLivekitUrl as customLivekitUrlSetting, + customTransport as customTransportSetting, advancedScreenShare as advancedScreenShareSetting, screenShareResolution as screenShareResolutionSetting, screenShareFramerate as screenShareFramerateSetting, @@ -167,16 +167,18 @@ export const DeveloperSettingsTab: FC = ({ enableExtendedLivekitLogsSetting, ); - const [customLivekitUrlUpdateError, setCustomLivekitUrlUpdateError] = - useState(null); - const [customLivekitUrl, setCustomLivekitUrl] = useSetting( - customLivekitUrlSetting, + const [customTransportUpdateError, setCustomLivekitUrlUpdateError] = useState< + string | null + >(null); + const [customTransport, setCustomTransport] = useSetting( + customTransportSetting, + ); + const [customTransportTextBuffer, setCustomLivekitUrlTextBuffer] = useState( + () => JSON.stringify(customTransport), ); - const [customLivekitUrlTextBuffer, setCustomLivekitUrlTextBuffer] = - useState(customLivekitUrl); useEffect(() => { - setCustomLivekitUrlTextBuffer(customLivekitUrl); - }, [customLivekitUrl]); + setCustomLivekitUrlTextBuffer(JSON.stringify(customTransport)); + }, [customTransport]); const [muteAllAudio, setMuteAllAudio] = useSetting(muteAllAudioSetting); @@ -491,23 +493,23 @@ export const DeveloperSettingsTab: FC = ({ e.preventDefault()} helpLabel={ - customLivekitUrl === null - ? t("developer_mode.custom_livekit_url.from_config") - : t("developer_mode.custom_livekit_url.current_url") + - customLivekitUrl + customTransport === null + ? t("developer_mode.custom_transport.from_config") + : t("developer_mode.custom_transport.current") + + JSON.stringify(customTransport) } - label={t("developer_mode.custom_livekit_url.label")} - saveButtonLabel={t("developer_mode.custom_livekit_url.save")} - savingLabel={t("developer_mode.custom_livekit_url.saving")} - cancelButtonLabel={t("developer_mode.custom_livekit_url.reset")} + label={t("developer_mode.custom_transport.label")} + saveButtonLabel={t("developer_mode.custom_transport.save")} + savingLabel={t("developer_mode.custom_transport.saving")} + cancelButtonLabel={t("developer_mode.custom_transport.reset")} onSave={useCallback( async (e: React.FormEvent): Promise => { if ( roomId === undefined || - customLivekitUrlTextBuffer === "" || - customLivekitUrlTextBuffer === null + customTransportTextBuffer === "" || + customTransportTextBuffer === null ) { - setCustomLivekitUrl(null); + setCustomTransport(null); return; } @@ -518,13 +520,12 @@ export const DeveloperSettingsTab: FC = ({ if (userId === null || deviceId === null) { throw new Error("Invalid user or device ID"); } + const transport = JSON.parse(customTransportTextBuffer); + // Validate the transport by attempting to get a token for it await getSFUConfig({ client, membership: { userId, deviceId, memberId: "" }, - transport: { - type: "livekit", - livekit_service_url: customLivekitUrlTextBuffer, - }, + transport, serverName: client.getDomain()!, roomId, slotId: "org.example.fake-slot#room", @@ -532,14 +533,16 @@ export const DeveloperSettingsTab: FC = ({ logger, }); setCustomLivekitUrlUpdateError(null); - setCustomLivekitUrl(customLivekitUrlTextBuffer); + setCustomTransport(transport); } catch { - setCustomLivekitUrlUpdateError("invalid URL (did not update)"); + setCustomLivekitUrlUpdateError( + "invalid transport (did not update)", + ); } }, - [customLivekitUrlTextBuffer, setCustomLivekitUrl, client, roomId], + [customTransportTextBuffer, setCustomTransport, client, roomId], )} - value={customLivekitUrlTextBuffer ?? ""} + value={customTransportTextBuffer ?? ""} onChange={useCallback( (event: ChangeEvent): void => { setCustomLivekitUrlTextBuffer(event.target.value); @@ -548,14 +551,14 @@ export const DeveloperSettingsTab: FC = ({ )} onCancel={useCallback( (e: React.FormEvent) => { - setCustomLivekitUrl(null); + setCustomTransport(null); }, - [setCustomLivekitUrl], + [setCustomTransport], )} - serverInvalid={customLivekitUrlUpdateError !== null} + serverInvalid={customTransportUpdateError !== null} > - {customLivekitUrlUpdateError !== null && ( - {customLivekitUrlUpdateError} + {customTransportUpdateError !== null && ( + {customTransportUpdateError} )} diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 933ac66f5..733068acc 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -59,7 +59,7 @@ interface Props { livekitRooms?: { room: LivekitRoom; url: string; - isLocal?: boolean; + role: "publisher" | "subscriber"; }[]; /** Only available while in a call. Used by the developer tab. */ developerSettingsVm?: ViewModel; diff --git a/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap b/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap index 63a68ac4b..6bfc9502f 100644 --- a/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap +++ b/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap @@ -239,7 +239,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = ` class="_label_1o4d9_60" for="radix-_r_7_" > - Custom Livekit-url + Custom transport JSON
renders and matches snapshot 1`] = ` id="radix-_r_7_" name="input" title="" - value="" + value="null" />
{ public constructor( @@ -153,8 +154,8 @@ export const matrixRTCMode = new Setting( MatrixRTCMode.Compatibility, ); -export const customLivekitUrl = new Setting( - "custom-livekit-url", +export const customTransport = new Setting( + "custom-transport", null, ); diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index 0b3caa42e..ed6654e2f 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -19,7 +19,7 @@ import fetchMock from "fetch-mock"; import { mockConfig } from "../../../utils/test"; import { getLocalTransport } from "./LocalTransport"; import { MatrixRTCTransportMissingError } from "../../../utils/errors"; -import { customLivekitUrl } from "../../../settings/settings"; +import { customTransport } from "../../../settings/settings"; import { type MatrixClient } from "matrix-js-sdk"; describe("LocalTransport", () => { @@ -60,7 +60,7 @@ describe("LocalTransport", () => { >; beforeEach(() => { mockConfig({}); - customLivekitUrl.setValue(customLivekitUrl.defaultValue); + customTransport.setValue(customTransport.defaultValue); client = { getDomain: vi.fn().mockReturnValue("example.org"), _unstable_getRTCTransports: vi.fn().mockResolvedValue([]), @@ -86,7 +86,10 @@ describe("LocalTransport", () => { }); it("supports getting transport via user settings", async () => { - customLivekitUrl.setValue("https://lk.example.org"); + customTransport.setValue({ + type: "livekit", + livekit_service_url: "https://lk.example.org", + }); expect(await getLocalTransport(client)).toStrictEqual({ transport: { diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index feb8aee6f..3b43f8112 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -5,13 +5,12 @@ SPDX-License-IdFentifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type MatrixClient } from "matrix-js-sdk"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { Config } from "../../../config/Config.ts"; import { MatrixRTCTransportMissingError } from "../../../utils/errors.ts"; -import { customLivekitUrl } from "../../../settings/settings.ts"; +import { customTransport as customTransportSetting } from "../../../settings/settings.ts"; import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts"; import { type TransportLocator } from "../../../livekit/auth/types.ts"; @@ -30,12 +29,11 @@ export async function getLocalTransport( resolvedConfig: Config.get(), logger: rootLogger.getChild("[LocalTransport]"), }); - const customUrl = customLivekitUrl.value$.value; - // Respect the user's custom URL, if set - const transport: UnstableLivekitTransport | null = customUrl - ? { type: "livekit", livekit_service_url: customUrl } - : await discovery.discoverPreferredTransport(); + // Respect the user's custom transport, if set + const transport = + customTransportSetting.value$.value ?? + (await discovery.discoverPreferredTransport()); if (transport === null) throw new MatrixRTCTransportMissingError(serverName);