From b5f526f9728eb4e9f3b25ff729fa69602136a838 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Mon, 19 May 2025 12:33:32 +0200 Subject: [PATCH] Backport: Fix to-device encryption info label (#3275) * Fix to-device encryption info label (#3208) * Fix to-device encryption info label The label was shown also without checking that we use PerParticipantE2EE. Which is a prerequisite for toDevice transport. As a result the label was shown when not desired. * rename: useLiveKit -> useLivekit * make the settings naming consistent * lint The issue were caused by unifying the settings names. This change did not make it into the v0.11.0 branch and hence the backport needs to be adapted to use the old naming. --- src/livekit/{useLiveKit.ts => useLivekit.ts} | 2 +- src/room/GroupCallView.tsx | 2 +- src/room/InCallView.test.tsx | 249 ++++++++++++++++++ src/room/InCallView.tsx | 41 ++- src/room/ReactionAudioRenderer.test.tsx | 14 +- .../__snapshots__/InCallView.test.tsx.snap | 181 +++++++++++++ src/settings/DeveloperSettingsTab.tsx | 2 +- src/settings/SettingsModal.tsx | 2 +- src/settings/settings.ts | 4 +- src/useAudioContext.test.tsx | 2 +- src/useAudioContext.tsx | 4 +- src/utils/test.ts | 8 +- 12 files changed, 479 insertions(+), 32 deletions(-) rename src/livekit/{useLiveKit.ts => useLivekit.ts} (99%) create mode 100644 src/room/InCallView.test.tsx create mode 100644 src/room/__snapshots__/InCallView.test.tsx.snap diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLivekit.ts similarity index 99% rename from src/livekit/useLiveKit.ts rename to src/livekit/useLivekit.ts index 972f7756..7cb32f5f 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLivekit.ts @@ -48,7 +48,7 @@ interface UseLivekitResult { connState: ECConnectionState; } -export function useLiveKit( +export function useLivekit( rtcSession: MatrixRTCSession, muteStates: MuteStates, sfuConfig: SFUConfig | undefined, diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 4ea356bf..513b51ea 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -62,7 +62,7 @@ import { } from "../utils/errors.ts"; import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx"; import { - useExperimentalToDeviceTransportSetting, + useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, useNewMembershipManagerSetting as useNewMembershipManagerSetting, useSetting, } from "../settings/settings"; diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx new file mode 100644 index 00000000..66e070d6 --- /dev/null +++ b/src/room/InCallView.test.tsx @@ -0,0 +1,249 @@ +/* +Copyright 2025 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 { + beforeEach, + describe, + expect, + it, + type MockedFunction, + vi, +} from "vitest"; +import { act, render, type RenderResult } from "@testing-library/react"; +import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk"; +import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; +import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container"; +import { ConnectionState, type LocalParticipant } from "livekit-client"; +import { of } from "rxjs"; +import { BrowserRouter } from "react-router-dom"; +import { TooltipProvider } from "@vector-im/compound-web"; +import { + RoomAudioRenderer, + RoomContext, + useLocalParticipant, +} from "@livekit/components-react"; +import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport"; + +import { type MuteStates } from "./MuteStates"; +import { InCallView } from "./InCallView"; +import { + mockLivekitRoom, + mockLocalParticipant, + mockMatrixRoom, + mockMatrixRoomMember, + mockRemoteParticipant, + mockRtcMembership, + type MockRTCSession, +} from "../utils/test"; +import { E2eeType } from "../e2ee/e2eeType"; +import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; +import { alice, local } from "../utils/test-fixtures"; +import { useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting } from "../settings/settings"; +import { ReactionsSenderProvider } from "../reactions/useReactionsSender"; +import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; + +// vi.hoisted(() => { +// localStorage = {} as unknown as Storage; +// }); +vi.hoisted( + () => + (global.ImageData = class MockImageData { + public data: number[] = []; + } as unknown as typeof ImageData), +); + +vi.mock("../soundUtils"); +vi.mock("../useAudioContext"); +vi.mock("../tile/GridTile"); +vi.mock("../tile/SpotlightTile"); +vi.mock("@livekit/components-react"); +vi.mock("../e2ee/sharedKeyManagement"); +vi.mock("react-use-measure", () => ({ + default: (): [() => void, object] => [(): void => {}, {}], +})); + +const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC"); +const localParticipant = mockLocalParticipant({ + identity: "@local:example.org:AAAAAA", +}); +const remoteParticipant = mockRemoteParticipant({ + identity: "@alice:example.org:AAAAAA", +}); +const carol = mockMatrixRoomMember(localRtcMember); +const roomMembers = new Map([carol].map((p) => [p.userId, p])); + +const roomId = "!foo:bar"; +let useRoomEncryptionSystemMock: MockedFunction; +beforeEach(() => { + vi.clearAllMocks(); + // RoomAudioRenderer is tested separately. + ( + RoomAudioRenderer as MockedFunction + ).mockImplementation((_props) => { + return
mocked: RoomAudioRenderer
; + }); + ( + useLocalParticipant as MockedFunction + ).mockImplementation( + () => + ({ + isScreenShareEnabled: false, + localParticipant: localRtcMember as unknown as LocalParticipant, + }) as unknown as ReturnType, + ); + + useRoomEncryptionSystemMock = + useRoomEncryptionSystem as typeof useRoomEncryptionSystemMock; + useRoomEncryptionSystemMock.mockReturnValue({ kind: E2eeType.NONE }); +}); + +function createInCallView(): RenderResult & { + rtcSession: MockRTCSession; +} { + const client = { + getUser: () => null, + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + getRoom: (rId) => (rId === roomId ? room : null), + } as Partial as MatrixClient; + const room = mockMatrixRoom({ + relations: { + getChildEventsForEvent: () => + vi.mocked({ + getRelations: () => [], + }), + } as unknown as RelationsContainer, + client, + roomId, + getMember: (userId) => roomMembers.get(userId) ?? null, + getMxcAvatarUrl: () => null, + hasEncryptionStateEvent: vi.fn().mockReturnValue(true), + getCanonicalAlias: () => null, + currentState: { + getJoinRule: () => JoinRule.Invite, + } as Partial as RoomState, + }); + + const muteState = { + audio: { enabled: false }, + video: { enabled: false }, + } as MuteStates; + const livekitRoom = mockLivekitRoom( + { + localParticipant, + }, + { + remoteParticipants$: of([remoteParticipant]), + }, + ); + const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]); + + rtcSession.joined = true; + const renderResult = render( + + + + + + + + + , + ); + return { + ...renderResult, + rtcSession, + }; +} + +describe("InCallView", () => { + describe("rendering", () => { + it("renders", () => { + const { container } = createInCallView(); + expect(container).toMatchSnapshot(); + }); + }); + describe("toDevice label", () => { + it("is shown if setting activated and room encrypted", () => { + useRoomEncryptionSystemMock.mockReturnValue({ + kind: E2eeType.PER_PARTICIPANT, + }); + useExperimentalToDeviceTransportSetting.setValue(true); + const { getByText } = createInCallView(); + expect(getByText("using to Device key transport")).toBeInTheDocument(); + }); + + it("is not shown in unenecrypted room", () => { + useRoomEncryptionSystemMock.mockReturnValue({ + kind: E2eeType.NONE, + }); + useExperimentalToDeviceTransportSetting.setValue(true); + const { queryByText } = createInCallView(); + expect( + queryByText("using to Device key transport"), + ).not.toBeInTheDocument(); + }); + + it("is hidden once fallback was triggered", async () => { + useRoomEncryptionSystemMock.mockReturnValue({ + kind: E2eeType.PER_PARTICIPANT, + }); + useExperimentalToDeviceTransportSetting.setValue(true); + const { rtcSession, queryByText } = createInCallView(); + expect(queryByText("using to Device key transport")).toBeInTheDocument(); + expect(rtcSession).toBeDefined(); + await act(() => + rtcSession.emit(RoomAndToDeviceEvents.EnabledTransportsChanged, { + toDevice: true, + room: true, + }), + ); + expect( + queryByText("using to Device key transport"), + ).not.toBeInTheDocument(); + }); + it("is not shown if setting is disabled", () => { + useExperimentalToDeviceTransportSetting.setValue(false); + + useRoomEncryptionSystemMock.mockReturnValue({ + kind: E2eeType.PER_PARTICIPANT, + }); + const { queryByText } = createInCallView(); + expect( + queryByText("using to Device key transport"), + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index c337bc3c..e90f78d8 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -56,7 +56,7 @@ import { type OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; -import { useLiveKit } from "../livekit/useLiveKit"; +import { useLivekit } from "../livekit/useLivekit.ts"; import { useWakeLock } from "../useWakeLock"; import { useMergedRefs } from "../useMergedRefs"; import { type MuteStates } from "./MuteStates"; @@ -73,7 +73,10 @@ import { import { Grid, type TileProps } from "../grid/Grid"; import { useInitial } from "../useInitial"; import { SpotlightTile } from "../tile/SpotlightTile"; -import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { + useRoomEncryptionSystem, + type EncryptionSystem, +} from "../e2ee/sharedKeyManagement"; import { E2eeType } from "../e2ee/e2eeType"; import { makeGridLayout } from "../grid/GridLayout"; import { @@ -96,7 +99,7 @@ import { ReactionsOverlay } from "./ReactionsOverlay"; import { CallEventAudioRenderer } from "./CallEventAudioRenderer"; import { debugTileLayout as debugTileLayoutSetting, - useExperimentalToDeviceTransportSetting, + useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, useSetting, } from "../settings/settings"; import { ReactionsReader } from "../reactions/ReactionsReader"; @@ -114,7 +117,7 @@ export interface ActiveCallProps export const ActiveCall: FC = (props) => { const sfuConfig = useOpenIDSFU(props.client, props.rtcSession); - const { livekitRoom, connState } = useLiveKit( + const { livekitRoom, connState } = useLivekit( props.rtcSession, props.muteStates, sfuConfig, @@ -233,19 +236,29 @@ export const InCallView: FC = ({ room: livekitRoom, }); - const [toDeviceEncryptionSetting] = useSetting( - useExperimentalToDeviceTransportSetting, - ); - const [showToDeviceEncryption, setShowToDeviceEncryption] = useState( - () => toDeviceEncryptionSetting, - ); - useEffect(() => { - setShowToDeviceEncryption(toDeviceEncryptionSetting); - }, [toDeviceEncryptionSetting]); + // This seems like it might be enough logic to move it into the call view model? + const [didFallbackToRoomKey, setDidFallbackToRoomKey] = useState(false); + useTypedEventEmitter( rtcSession, RoomAndToDeviceEvents.EnabledTransportsChanged, - (enabled) => setShowToDeviceEncryption(enabled.to_device), + (enabled) => setDidFallbackToRoomKey(enabled.room), + ); + const [useExperimentalToDeviceTransport] = useSetting( + useExperimentalToDeviceTransportSetting, + ); + const encryptionSystem = useRoomEncryptionSystem(rtcSession.room.roomId); + + const showToDeviceEncryption = useMemo( + () => + useExperimentalToDeviceTransport && + encryptionSystem.kind === E2eeType.PER_PARTICIPANT && + !didFallbackToRoomKey, + [ + encryptionSystem.kind, + didFallbackToRoomKey, + useExperimentalToDeviceTransport, + ], ); const toggleMicrophone = useCallback( diff --git a/src/room/ReactionAudioRenderer.test.tsx b/src/room/ReactionAudioRenderer.test.tsx index fa7df166..c61cbd82 100644 --- a/src/room/ReactionAudioRenderer.test.tsx +++ b/src/room/ReactionAudioRenderer.test.tsx @@ -21,8 +21,8 @@ import { act, type ReactNode } from "react"; import { ReactionsAudioRenderer } from "./ReactionAudioRenderer"; import { - playReactionsSound, - soundEffectVolumeSetting, + playReactionsSound as playReactionsSoundSetting, + soundEffectVolume as soundEffectVolumeSetting, } from "../settings/settings"; import { useAudioContext } from "../useAudioContext"; import { GenericReaction, ReactionSet } from "../reactions"; @@ -50,7 +50,7 @@ vitest.mock("../soundUtils"); afterEach(() => { vitest.resetAllMocks(); - playReactionsSound.setValue(playReactionsSound.defaultValue); + playReactionsSoundSetting.setValue(playReactionsSoundSetting.defaultValue); soundEffectVolumeSetting.setValue(soundEffectVolumeSetting.defaultValue); }); @@ -74,7 +74,7 @@ beforeEach(() => { test("preloads all audio elements", () => { const { vm } = getBasicCallViewModelEnvironment([local, alice]); - playReactionsSound.setValue(true); + playReactionsSoundSetting.setValue(true); render(); expect(prefetchSounds).toHaveBeenCalledOnce(); }); @@ -84,7 +84,7 @@ test("will play an audio sound when there is a reaction", () => { local, alice, ]); - playReactionsSound.setValue(true); + playReactionsSoundSetting.setValue(true); render(); // Find the first reaction with a sound effect @@ -110,7 +110,7 @@ test("will play the generic audio sound when there is soundless reaction", () => local, alice, ]); - playReactionsSound.setValue(true); + playReactionsSoundSetting.setValue(true); render(); // Find the first reaction with a sound effect @@ -136,7 +136,7 @@ test("will play multiple audio sounds when there are multiple different reaction local, alice, ]); - playReactionsSound.setValue(true); + playReactionsSoundSetting.setValue(true); render(); // Find the first reaction with a sound effect diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap new file mode 100644 index 00000000..427973b6 --- /dev/null +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -0,0 +1,181 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`InCallView > rendering > renders 1`] = ` +
+
+
+
+ mocked: RoomAudioRenderer +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+`; diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index 67de0e0d..e3f50ebb 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -16,7 +16,7 @@ import { showNonMemberTiles as showNonMemberTilesSetting, showConnectionStats as showConnectionStatsSetting, useNewMembershipManagerSetting, - useExperimentalToDeviceTransportSetting, + useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, } from "./settings"; import type { MatrixClient } from "matrix-js-sdk"; import type { Room as LivekitRoom } from "livekit-client"; diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index b24674dc..b0a4b79e 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -23,7 +23,7 @@ import { import { widget } from "../widget"; import { useSetting, - soundEffectVolumeSetting, + soundEffectVolume as soundEffectVolumeSetting, backgroundBlur as backgroundBlurSetting, developerMode, } from "./settings"; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index a3b52c7a..6525f50d 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -110,7 +110,7 @@ export const playReactionsSound = new Setting( true, ); -export const soundEffectVolumeSetting = new Setting( +export const soundEffectVolume = new Setting( "sound-effect-volume", 0.5, ); @@ -120,7 +120,7 @@ export const useNewMembershipManagerSetting = new Setting( true, ); -export const useExperimentalToDeviceTransportSetting = new Setting( +export const useExperimentalToDeviceTransport = new Setting( "experimental-to-device-transport", true, ); diff --git a/src/useAudioContext.test.tsx b/src/useAudioContext.test.tsx index 29949bf8..92d3a947 100644 --- a/src/useAudioContext.test.tsx +++ b/src/useAudioContext.test.tsx @@ -12,7 +12,7 @@ import userEvent from "@testing-library/user-event"; import { deviceStub, MediaDevicesContext } from "./livekit/MediaDevicesContext"; import { useAudioContext } from "./useAudioContext"; -import { soundEffectVolumeSetting } from "./settings/settings"; +import { soundEffectVolume as soundEffectVolumeSetting } from "./settings/settings"; const staticSounds = Promise.resolve({ aSound: new ArrayBuffer(0), diff --git a/src/useAudioContext.tsx b/src/useAudioContext.tsx index d96b9fdc..7cd4ff39 100644 --- a/src/useAudioContext.tsx +++ b/src/useAudioContext.tsx @@ -9,7 +9,7 @@ import { logger } from "matrix-js-sdk/lib/logger"; import { useState, useEffect } from "react"; import { - soundEffectVolumeSetting as effectSoundVolumeSetting, + soundEffectVolume as soundEffectVolumeSetting, useSetting, } from "./settings/settings"; import { useMediaDevices } from "./livekit/MediaDevicesContext"; @@ -62,7 +62,7 @@ interface UseAudioContext { export function useAudioContext( props: Props, ): UseAudioContext | null { - const [effectSoundVolume] = useSetting(effectSoundVolumeSetting); + const [effectSoundVolume] = useSetting(soundEffectVolumeSetting); const devices = useMediaDevices(); const [audioContext, setAudioContext] = useState(); const [audioBuffers, setAudioBuffers] = useState>(); diff --git a/src/utils/test.ts b/src/utils/test.ts index 039b6983..6e1b5457 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -29,6 +29,10 @@ import { type Room as LivekitRoom, } from "livekit-client"; import { randomUUID } from "crypto"; +import { + type RoomAndToDeviceEvents, + type RoomAndToDeviceEventsHandlerMap, +} from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport"; import { LocalUserMediaViewModel, @@ -269,8 +273,8 @@ export function mockConfig(config: Partial = {}): void { } export class MockRTCSession extends TypedEventEmitter< - MatrixRTCSessionEvent, - MatrixRTCSessionEventHandlerMap + MatrixRTCSessionEvent | RoomAndToDeviceEvents, + MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap > { public readonly statistics = { counters: {},